Conversation
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
There was a problem hiding this comment.
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.
| if field.data.starts_with(b"tool_") { | ||
| return true; | ||
| } |
There was a problem hiding this comment.
[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.
| 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; | |
| } |
|
| continue; | ||
| } | ||
| if field.data.starts_with(b"tool_") { | ||
| return true; |
There was a problem hiding this 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
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.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Merging this PR will improve performance by 16.63%
|
| 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)
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:
\n0x0a0x20tool_call_id_example_abcdefghijktool_prefixA 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. Onmainthe 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 —
ExecServerMessagefield 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_DEPTHgoes 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
user_content_that_parses_as_protobuf_is_not_a_tool_call, which fails onmainwithleft: Some(7), right: Noneand passes here.call_id_with_non_utf8_tail_is_still_detected.cargo fmt --all --checkandcargo clippy --all-targets --all-features -- -D warningsclean.websocket_rate_limits_event_records_account_quota, which fails identically on an unmodifiedupstream/maincheckout 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
MAX_TOOL_ID_SCAN_DEPTHand the recursive walk.Refs #426
Written for commit d20b31b. Summary will update on new commits.