Skip to content

fix(omo-ai): strip cursor-cli tool protocol frames from assistant messages - #7192

Closed
AceRothstein71 wants to merge 2 commits into
code-yeongyu:devfrom
AceRothstein71:issue/7169-cursor-oauth-tool-leak
Closed

fix(omo-ai): strip cursor-cli tool protocol frames from assistant messages#7192
AceRothstein71 wants to merge 2 commits into
code-yeongyu:devfrom
AceRothstein71:issue/7169-cursor-oauth-tool-leak

Conversation

@AceRothstein71

@AceRothstein71 AceRothstein71 commented Aug 24, 2026

Copy link
Copy Markdown

What

Adds the cursor-tool-frame-filter component to the omo-senpi adapter. At the message_end boundary it strips Cursor CLI tool protocol frames (<cursor-cli-tool>{...}</cursor-cli-tool>) from assistant text blocks and drops text blocks that held only frames, then returns the sanitized message as the MessageEndEventResult.message replacement. Also registers the component in the extension composition and rebuilds the committed extension bundle.

Why

Issue #7169: with the cursor-cli-oauth provider selected, the pinned @code-yeongyu/senpi@2026.8.23 engine serializes every started and completed Cursor tool_call event through renderToolFrame() into <cursor-cli-tool> JSON strings and routes them via pushTextDelta(), so provider-internal protocol lands in assistant prose and persists into session JSONL. The upstream fix (senpi PR #1107) is not yet released, so the exact pin cannot be bumped; this is the adapter-side boundary fix in the only repo-owned code that runs inside the senpi process.

Design notes:

  • Frames are removed at the message persistence boundary, not trimmed at display time: senpi applies the message_end replacement via _replaceMessageInPlace(event.message, ...) before sessionManager.appendMessage(event.message), so cleaned text is what gets stored and rendered (verified against the installed pinned dist).
  • Frames are never converted to host toolCall blocks, so Cursor-executed operations cannot run twice.
  • Stripping is conservative: fenced code blocks are skipped entirely, and a span only counts as a frame when its payload starts with { and either parses as JSON or carries the upstream over-budget ...[truncated] suffix. Prose and code examples that merely resemble the protocol survive byte-identical.
  • Gated by omo-senpi-cursor-tool-frame-filter-disabled.

Relation to #7115: same class of bug (internal provider/host protocol surfacing as assistant-visible content in an oauth lane). That issue's fix crosses the Senpi dependency boundary and remains open; this fix uses the same lesson, sanitize at the provider boundary before content reaches assistant messages, applied at the seam this repo controls.

Verified

  • RED first: the new regression tests fail before the implementation exists (module not found for ./filter / ./index).
  • GREEN after: 13/13 new tests pass (bun test packages/omo-senpi/src/components/cursor-tool-frame-filter/). Fixtures byte-mirror the pinned dist renderToolFrame() output including the truncated over-budget shape; assertions cover frame removal, verbatim prose, byte-identical protocol-resembling code fences, empty-content collapse for frame-only turns, untouched user messages, and the disable flag.
  • Scoped suites: extension composition 64/64, components 2013 pass with one pre-existing environment failure (ulw-loop cross-session probe fails identically on the clean base, verified via stash), installer + skills sync 34/34, src-root audit gates (bundle-size, bundle-purity, package-shape, plugin-manifest, runtime audits) all green.
  • Typecheck: tsgo --noEmit -p packages/omo-senpi/tsconfig.json exit 0.
  • QA evidence recorded under .omo/evidence/20260824-cursor-oauth-tool-leak/ (local; the directory is gitignored by current repo policy), including the upstream leak source excerpt and the persistence-ordering proof lines from the installed senpi dist.
  • Environment note: the full test:senpi chain cannot start on this machine because its first step (build:materialize-frontend) hits a pre-existing unfetchable submodule revision (shared-skills/upstreams/open-design); every downstream gate of that chain was executed individually instead.

Risk

Low. The component is additive, flag-gated, and returns a replacement only when a frame was actually removed. Residual risk is a future senpi release changing the private frame format; the fixtures name the exact upstream file and function so drift is reviewable, and the component can be retired once the pin includes the upstream fix.

Fixes #7169


Summary by cubic

Removes Cursor CLI tool protocol frames from assistant messages to prevent internal cursor-cli-oauth events from leaking into prose and session history. Old: <cursor-cli-tool>{...}</cursor-cli-tool> strings appeared in assistant text; New: frames are stripped at message_end and frame-only blocks are dropped before persistence. Fixes #7169.

  • Registers cursor-tool-frame-filter in omo-senpi: sanitizes assistant text blocks, never converts frames to host tool calls, and ensures storage/rendering use the sanitized content.
  • Conservative stripping: skips fenced code blocks; only removes spans that start with { and either parse as JSON or end with ...[truncated].
  • Feature flag: disable via omo-senpi-cursor-tool-frame-filter-disabled.
  • Review focus: src/components/cursor-tool-frame-filter/filter.ts, src/components/cursor-tool-frame-filter/index.ts, and registration in src/extension/component-list.ts; committed bundle updated under plugin/extensions/*.
  • Tests added cover frame removal, truncated bodies, fence preservation, frame-only turns collapsing to empty content, user messages untouched, and the disable flag.

Written for commit 3a882c6. Summary will update on new commits.

Review in cubic

ROM Builder added 2 commits August 24, 2026 18:51
…sages

The pinned senpi 2026.8.23 cursor-cli-oauth provider serializes every started
and completed Cursor tool_call event as a <cursor-cli-tool>{...}</cursor-cli-tool>
text delta, so provider-internal JSON landed in assistant prose and persisted
session history (code-yeongyu#7169). The upstream fix (senpi PR code-yeongyu#1107) is unreleased, so the
exact pin cannot be bumped yet.

Add the cursor-tool-frame-filter component: at the message_end boundary (which
senpi applies before session persistence) it strips frame spans from assistant
text blocks and drops blocks that held only frames. Frames are never converted
to host toolCall blocks because Cursor already executed those tools in its own
subprocess. Stripping skips fenced code blocks and requires a brace payload
(valid JSON or the upstream ...[truncated] shape), so prose and code examples
that merely resemble the protocol survive byte-identical.

Gated by omo-senpi-cursor-tool-frame-filter-disabled. Regression fixtures mirror
the exact renderToolFrame serialization of the pinned dist, including the
over-budget truncated shape.
Regenerated via plugin/scripts/build-extension.mjs to include the new
cursor-tool-frame-filter component in the committed omo.js bundle.
@github-actions github-actions Bot added the omo-senpi Changes under packages/omo-senpi label Aug 24, 2026
@MoerAI

MoerAI commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Closing as superseded by upstream senpi#1107, which is included in the @code-yeongyu/senpi@2026.8.31 version pinned by current dev:

This fallback should not be revived unchanged. It accepts any JSON object between tags rather than validating the Cursor protocol schema, so it can delete legitimate inline examples and corrupt adjacent non-frame text.

Keep #7169 open only if the leak reproduces against the current Senpi pin. Reopen this PR only with such a reproduction and a schema-validating parser that preserves legitimate content and tool-call/result pairing.

@MoerAI MoerAI closed this Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

omo-senpi Changes under packages/omo-senpi

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(omo-ai): cursor-cli-oauth leaks raw tool protocol into assistant text

2 participants