Skip to content

fix(cursor): stop re-parsing user content as a tool-call id - #583

Open
r-uben wants to merge 1 commit into
pleaseai:mainfrom
r-uben:fix/426-cursor-depth0-tool-id-scan
Open

r-uben wants to merge 1 commit into
pleaseai:mainfrom
r-uben:fix/426-cursor-depth0-tool-id-scan

Conversation

@r-uben

@r-uben r-uben commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Summary

The unbridged built-in tool-call scan added in #410 recursed into every length-delimited field. Protobuf does not distinguish a nested message from a string, so that walk re-parsed whatever a field held — including the file contents a built-in read carries — as wire format, and reported a tool call found inside the user's own data.

Raised by gemini-code-assist on #410 and filed as #426, where the remedy was left pending a measurement. The false positive turns out to be reproducible, which is what this PR adds along with the fix.

The content that trips it

Ordinary text, not a crafted payload:

byte value read as protobuf
\n 0x0a tag: field 1, wire type 2
0x20 length: 32
tool_call_id_example_abcdefghijk 32 bytes the value, matching the tool_ prefix

A blank line followed by a one-space-indented line beginning with tool_. A YAML fragment, a diff hunk or a pasted log will produce it. On main the turn then fails with the 502 the scan exists to raise — on a turn that was working.

Why depth 0 is enough

The recursion was never needed to reach a real id. The captured shape places it at depth 0 — ExecServerMessage field 7 = { 1: <path>, 2: "tool_<uuid>" } — so scanning direct fields only still detects every captured turn while leaving user content unparsed. MAX_TOOL_ID_SCAN_DEPTH goes with it.

The two narrower variants recorded in the function's doc (field-position pinning, uuid-shaped body) narrowed the id body and both leaked silent turns. Nesting is a separate axis, and it is the one that changes here; the body match stays as wide as it was.

Test plan

  • New regression test user_content_that_parses_as_protobuf_is_not_a_tool_call, which fails on main with left: Some(7), right: None and passes here.
  • All four detection tests from fix(cursor): surface built-in tool calls instead of dropping the turn #410 pass unchanged, including call_id_with_non_utf8_tail_is_still_detected.
  • Cursor suite: 200 passed, 0 failed.
  • cargo fmt --all --check and cargo clippy --all-targets --all-features -- -D warnings clean.
  • Full workspace suite passes apart from websocket_rate_limits_event_records_account_quota, which fails identically on an unmodified upstream/main checkout in this environment and is unrelated to this change.

Not covered

This is not the live-upstream comparison #426 asks for. I have not run the depth-0 variant through the harness that produced the 38-turn figure, so the leak question is not settled on that evidence. What has changed is that the competing risk is no longer hypothetical: it is reproducible against shipped code through ordinary file content. I can run the live comparison when I next have Cursor access, and am happy to hold this until then.

Refs #426


Summary by cubic

Fixes the unbridged built-in tool-call scan so it no longer re-parses user content as protobuf, which caused false tool-call detections that failed working turns. The scan recursed into every length-delimited field, but protobuf doesn't distinguish strings from nested messages, so ordinary file content could be misread as a tool call. Now the scan only checks direct fields, since the captured id always sits at depth 0. The body match stays as wide as before.

Bug Fixes

  • Removes MAX_TOOL_ID_SCAN_DEPTH and the recursive walk.
  • Adds a regression test for user content that parses as protobuf.

Refs #426

Written for commit d20b31b. Summary will update on new commits.

The unbridged built-in tool-call scan recursed into every length-delimited
field. Protobuf does not distinguish a nested message from a string, so that
walk re-parsed whatever a field held -- including the file contents a built-in
read carries -- as wire format, and reported a tool call found inside the
user's own data.

The content needed to trip it is ordinary text, not a crafted payload: `\n` is
0x0a (field 1, wire type 2) and a space is a length of 32, so a blank line
followed by a one-space-indented line beginning with `tool_` reads as a valid
tag, length and `tool_` prefix. A YAML fragment, diff hunk or pasted log can
produce it. The turn then fails with the 502 the scan exists to raise, on a
turn that was working.

The recursion was never needed to reach a real id. The captured shape places it
at depth 0 -- ExecServerMessage field 7 = { 1: <path>, 2: "tool_<uuid>" } -- so
scanning direct fields only detects every captured turn while leaving user
content unparsed. MAX_TOOL_ID_SCAN_DEPTH goes with it.

The two narrower variants recorded in the function's doc narrowed the id *body*
and both leaked; nesting is a separate axis and is what changes here.

Refs pleaseai#426

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request removes recursion from the tool call ID scanner in src/adapters/cursor/agent.rs to prevent re-parsing nested user content as protobuf wire format, resolving false positives where user data was incorrectly flagged as a tool call. A regression test has been added to verify this behavior. The reviewer suggests adding a length check on the scanned field data to further prevent false positives from files starting with the "tool_" prefix.

Comment on lines 921 to 923
if field.data.starts_with(b"tool_") {
return true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

[MEDIUM] Limit the length of the scanned tool call ID to prevent false positives on files starting with "tool_"

Problem: While removing recursion prevents false positives from tool_ prefixes appearing deep within file contents, a direct field carrying file content (such as a built-in write tool call) could still trigger a false positive if the file content itself starts with the tool_ prefix.
Rationale: A legitimate tool call ID is always short (typically a UUID, well under 128 bytes), whereas file contents can be much larger. Enforcing a maximum length limit using the existing MAX_TOOL_CALL_ID_LEN constant makes the prefix check significantly more robust.
Suggestion: Add a length check to field.data before matching the prefix.

Suggested change
if field.data.starts_with(b"tool_") {
return true;
}
if field.data.len() <= MAX_TOOL_CALL_ID_LEN && field.data.starts_with(b"tool_") {
return true;
}

@greptile-apps

greptile-apps Bot commented Sep 17, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

The PR is not yet safe to merge because a built-in read whose direct content starts with tool_ still fails as an unbridged tool call.

Fix All in Claude CodeFindings

  1. P1 Direct content still misclassified
Fix with agent prompt
### Issue 1
src/adapters/cursor/agent.rs:922
A built-in read can carry user content in a direct length-delimited field. If that content begins with `tool_`, this prefix-only check treats it as a tool-call ID and terminates an otherwise working turn with the unbridged-tool 502. The new regression starts its content with a newline and space, so it does not cover this direct match. Distinguish call-ID fields from user-content fields and add coverage for content beginning directly with `tool_`.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

This PR removes recursive protobuf scanning from Cursor’s unbridged built-in-tool detector and adds a regression test for file content whose bytes resemble a nested protobuf tool-call ID.

  • Restricts tool-ID detection to direct length-delimited fields.
  • Removes the recursive scan-depth limit.
  • Adds focused coverage for the reported nested-content false positive.
  • A residual direct-field false positive remains when user content begins with tool_.

Reviews (1) · Last reviewed commit: "fix(cursor): stop re-parsing user conten..."

continue;
}
if field.data.starts_with(b"tool_") {
return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Direct content still misclassified

A built-in read can carry user content in a direct length-delimited field. If that content begins with tool_, this prefix-only check treats it as a tool-call ID and terminates an otherwise working turn with the unbridged-tool 502. The new regression starts its content with a newline and space, so it does not cover this direct match. Distinguish call-ID fields from user-content fields and add coverage for content beginning directly with tool_.

Knowledge Base Used: Protocol and model translation

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/adapters/cursor/agent.rs
Line: 922

Comment:
**Direct content still misclassified**

A built-in read can carry user content in a direct length-delimited field. If that content begins with `tool_`, this prefix-only check treats it as a tool-call ID and terminates an otherwise working turn with the unbridged-tool 502. The new regression starts its content with a newline and space, so it does not cover this direct match. Distinguish call-ID fields from user-content fields and add coverage for content beginning directly with `tool_`.

**Knowledge Base Used:** [Protocol and model translation](https://app.greptile.com/passionfactory/-/custom-context/knowledge-base/pleaseai/shunt/-/docs/protocol-translation.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code

@codecov

codecov Bot commented Sep 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed

codspeed Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will improve performance by 16.63%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
✅ 100 untouched benchmarks

Performance Changes

Benchmark BASE HEAD Efficiency
parse_body_to_value[200] 2.6 ms 2.2 ms +16.63%

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing r-uben:fix/426-cursor-depth0-tool-id-scan (d20b31b) with main (02feba9)

Open in CodSpeed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant