Skip to content

fix(cursor): surface built-in tool calls instead of dropping the turn - #410

Open
r-uben wants to merge 5 commits into
pleaseai:mainfrom
r-uben:fix/cursor-builtin-tool-call-dropped
Open

fix(cursor): surface built-in tool calls instead of dropping the turn#410
r-uben wants to merge 5 commits into
pleaseai:mainfrom
r-uben:fix/cursor-builtin-tool-call-dropped

Conversation

@r-uben

@r-uben r-uben commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Composer chooses per turn whether to invoke a bridged caller tool or one of Cursor's own built-in tools. A bridged call arrives as ExecServerMessage.mcp_args (field 11) and decodes correctly. A built-in call arrives under a tool-specific field carrying only its arguments and a tool_<uuid> call id, with no tool name — observed for the built-in file read as ExecServerMessage field 7: {1: "<path>", 2: "tool_<uuid>"}.

extract_tool_call matched field 11 only, so a built-in call fell through to the End branch: the turn closed with end_turn and no tool_use. The visible symptom is a model that announces a tool call ("I'll read /etc/hostname using the Read tool") and then does nothing — which makes the Cursor adapter unusable for agentic clients, since roughly half of tool-bearing turns silently no-op.

shunt cannot bridge these calls: the caller owns the filesystem, and the frame carries no name to map onto a caller tool. This PR detects them and fails the turn with an explicit error, converting a silent truncation into a visible one. Detection keys on the tool_ call-id prefix appearing outside field 11.

Evidence

Captured raw Connect frames against the live upstream (throwaway instrumented build, not included here). Correlation over six turns was exact:

turn outcome field 7 (built-in) field 11 (mcp)
1 no tool_use 1 0
2 no tool_use 1 0
3–6 tool_use 0 1

Across all captured turns, only fields 7 and 11 ever carry a tool_* id — which is what makes the prefix a safe discriminator without enumerating Cursor's built-in catalog.

After the fix, ten live turns on the same request: 5 bridged tool calls, 5 explicit errors, 0 silent end_turns.

Milestone / spec

None — this is a wire-behavior bug in the agent.v1 Cursor path, not a spec deviation. Related open issues: #269, #29, #59 (all Cursor adapter, none covering this).

Checklist

  • cargo build passes
  • cargo test passes (1701 passed, 0 failed; three new unit tests, no network)
  • cargo clippy --all-targets --all-features -- -D warnings clean
  • cargo fmt --all --check clean
  • Source files stay under 500 lines — src/adapters/cursor/agent.rs was already ~1450 lines before this change (tracked by Split oversized Cursor adapter files (>500 lines) into focused modules #59); this adds to it rather than fixing it. Happy to land it elsewhere if you'd prefer.
  • English only; matches surrounding style
  • Frozen spec in docs/ updated if this change deviates from it (n/a)
  • User-facing docs updated — site/src/content/docs/providers/cursor.mdx gained a note on the built-in-tool error (no locale copies of that page exist)
  • Any new GitHub Action is pinned to a full commit SHA (n/a)

Notes for reviewers

  • The detector is a heuristic, and that is the main thing worth scrutiny: it treats any tool_* id outside field 11 as an unbridged built-in call. It's backed by observation, not by a schema. If the real ExecServerMessage descriptor is available, an explicit field allowlist would be strictly better.
  • Only field 7 (file read) was observed. Other built-ins presumably occupy other field numbers; the prefix rule should catch them, but that is untested.
  • This makes the failure visible, not fixed — Composer still picks its built-ins about half the time. The real fix is presumably a request-side flag that suppresses built-in tools when MCP tools are supplied, which I could not identify on the current wire. Follow-up territory.

Summary by cubic

Surfaces Cursor built-in tool calls as explicit 502 errors instead of closing the turn with end_turn and no tool_use. We detect a tool_* call id outside ExecServerMessage.mcp_args (field 11) and fail the turn so the issue is visible.

  • Matches the raw-byte tool_ prefix outside field 11 with bounded recursion; ignores plain-text frames and excludes the bridged mcp_args path.
  • Error text clarifies retry is the only mitigation; detection is request-agnostic and can trigger even when no tools were advertised.
  • Adds tests, including a non-UTF-8 call-id tail; builds the message with concat!.
  • Updates site/src/content/docs/providers/cursor.mdx and README.md to document the explicit error and the non-deterministic built-in path.

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

@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 improves tool-call handling in the antigravity-cli and cursor adapters by explicitly failing requests with clear errors when tool bridging is not supported, rather than silently ignoring them or truncating the turn. Specifically, antigravity-cli now rejects caller-supplied tools with a 400 error, and the cursor adapter detects and fails on unbridged built-in tool calls with a 502 error. The code review feedback recommends optimizing the tool ID prefix check in the Cursor adapter by avoiding full UTF-8 validation on byte slices, and suggests using the concat! macro for multi-line string literals in both adapters to prevent issues with IDEs stripping fragile trailing whitespaces.

Comment thread src/adapters/cursor/agent.rs
Comment thread src/adapters/antigravity/mod.rs Outdated
Comment on lines +649 to +655
let message = "The deprecated `antigravity-cli` transport cannot use caller-supplied \
tools. It runs the local `agy` binary, which resolves its own tool calls and never \
returns a tool_use block, so this request would otherwise get a text-only reply \
that silently ignored them. Send the task as a plain prompt and let agy do the \
work, or route this model at the native `antigravity` provider (or `gemini`), \
which do forward tools."
.to_string();

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] Fragile trailing whitespace in multi-line string literal

Problem: The multi-line string literal uses trailing spaces before the line-continuation backslashes (\) to separate words across lines. This is extremely fragile because many IDEs and code formatters are configured to automatically strip trailing whitespace on save, which would silently remove these spaces and cause words to run together (e.g., "caller-suppliedtools").

Rationale: Organization Style Guide §1 (Maintainability) and §12.2 (R1 - Cognitive Overload / Accidental Complexity).

Suggestion: Use concat! to safely construct the multi-line string with explicit spaces at the end of each segment, making it robust against trailing whitespace stripping.

    let message = concat!(
        "The deprecated `antigravity-cli` transport cannot use caller-supplied tools. ",
        "It runs the local `agy` binary, which resolves its own tool calls and never ",
        "returns a tool_use block, so this request would otherwise get a text-only reply ",
        "that silently ignored them. Send the task as a plain prompt and let agy do the ",
        "work, or route this model at the native `antigravity` provider (or `gemini`), ",
        "which do forward tools."
    )
    .to_string();
References
  1. Organization Style Guide §1 (Maintainability) and §12.2 (R1 - Cognitive Overload / Accidental Complexity) (link)

Comment thread src/adapters/cursor/agent.rs
@greptile-apps

greptile-apps Bot commented Aug 20, 2026

Copy link
Copy Markdown

Greptile Summary

The PR changes Cursor’s response decoding so otherwise-unbridgeable built-in tool calls produce an explicit error instead of a silent successful turn.

  • Detects tool_* call identifiers in non-MCP ExecServerMessage fields with bounded recursive protobuf scanning.
  • Adds focused tests for built-in, bridged, non-UTF-8, and ordinary text frames.
  • Documents the explicit failure and retry behavior in the English README and Cursor provider page.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
src/adapters/cursor/agent.rs Adds bounded detection of unbridgeable Cursor built-in tool calls, explicit error handling, and focused decoder tests.
site/src/content/docs/providers/cursor.mdx Documents how built-in calls are surfaced and that retrying is the available mitigation.
README.md Updates the English Cursor overview with the new explicit failure behavior.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Cursor ExecServerMessage] --> B{MCP field 11?}
  B -->|Yes| C[Emit bridged tool call]
  B -->|No| D{Nested tool_* call id?}
  D -->|Yes| E[Emit explicit gateway error]
  D -->|No| F[Continue decoding text, reasoning, or turn end]
Loading

Reviews (6): Last reviewed commit: "docs(cursor): note the unbridgeable buil..." | Re-trigger Greptile

Comment thread README.md Outdated
**Antigravity has two transports.** The `antigravity` provider talks to the Google Antigravity backend over HTTP, authenticated with `shunt login antigravity` — a Google authorization-code flow using Antigravity's own OAuth client and scopes, so a Gemini CLI login cannot be reused for it. It speaks the same Code Assist protocol as the `gemini` provider and currently serves the Gemini-family Antigravity models; the Claude models Antigravity also offers need request rewrites that are not implemented yet (#368).

**`antigravity-cli` is deprecated and is arbitrary code execution.** It runs the local `agy` binary in agentic mode: the CLI uses its own tools to do the work and shunt streams its progress back as Anthropic SSE. A `tools` array on the request is therefore not forwarded and no `tool_use` block is ever returned. Because a non-interactive run cannot answer a permission prompt, `agy` runs with `--dangerously-skip-permissions`, so **treat this provider as arbitrary code execution as the user running shunt**. Two settings bound it: `sandbox` (default `true`) passes `--sandbox`, which keeps reads and writes inside the workspace and is what actually contains the agent; `workspace_roots` decides only where it may *start*, gating the `Working directory:` path taken from the request's system prompt (client-controlled text) to canonicalized paths under roots you list. Keep the sandbox on and the bind on loopback. Prefer the `antigravity` provider above, which needs none of this. See the [providers guide](https://shunt.dev/guides/providers/).
**`antigravity-cli` is deprecated and is arbitrary code execution.** It runs the local `agy` binary in agentic mode: the CLI uses its own tools to do the work and shunt streams its progress back as Anthropic SSE. It can therefore never return a `tool_use` block, so a request that actually asks for one — a non-empty `tools` array, or a `tool_choice` of `any`/`tool` — is refused with a `400` rather than silently answered as text. `tool_choice: none` is exempt even alongside `tools`, and so is `auto` with no tools, since neither obliges a tool call. Because a non-interactive run cannot answer a permission prompt, `agy` runs with `--dangerously-skip-permissions`, so **treat this provider as arbitrary code execution as the user running shunt**. Two settings bound it: `sandbox` (default `true`) passes `--sandbox`, which keeps reads and writes inside the workspace and is what actually contains the agent; `workspace_roots` decides only where it may *start*, gating the `Working directory:` path taken from the request's system prompt (client-controlled text) to canonicalized paths under roots you list. Keep the sandbox on and the bind on loopback. Prefer the `antigravity` provider above, which needs none of this. See the [providers guide](https://shunt.dev/guides/providers/).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Root translations remain stale

The English README now documents that tool-bearing antigravity-cli requests return 400, but the maintained Korean, Japanese, and Chinese root READMEs retain the previous behavior. Update all three translations in this PR so localized documentation does not continue telling users that caller tools are merely ignored.

Context Used: CLAUDE.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: README.md
Line: 154

Comment:
**Root translations remain stale**

The English README now documents that tool-bearing `antigravity-cli` requests return `400`, but the maintained Korean, Japanese, and Chinese root READMEs retain the previous behavior. Update all three translations in this PR so localized documentation does not continue telling users that caller tools are merely ignored.

**Context Used:** CLAUDE.md ([source](https://github.com/pleaseai/shunt/blob/main/CLAUDE.md))

---

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

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6f432ead3b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread README.md Outdated
**Antigravity has two transports.** The `antigravity` provider talks to the Google Antigravity backend over HTTP, authenticated with `shunt login antigravity` — a Google authorization-code flow using Antigravity's own OAuth client and scopes, so a Gemini CLI login cannot be reused for it. It speaks the same Code Assist protocol as the `gemini` provider and currently serves the Gemini-family Antigravity models; the Claude models Antigravity also offers need request rewrites that are not implemented yet (#368).

**`antigravity-cli` is deprecated and is arbitrary code execution.** It runs the local `agy` binary in agentic mode: the CLI uses its own tools to do the work and shunt streams its progress back as Anthropic SSE. A `tools` array on the request is therefore not forwarded and no `tool_use` block is ever returned. Because a non-interactive run cannot answer a permission prompt, `agy` runs with `--dangerously-skip-permissions`, so **treat this provider as arbitrary code execution as the user running shunt**. Two settings bound it: `sandbox` (default `true`) passes `--sandbox`, which keeps reads and writes inside the workspace and is what actually contains the agent; `workspace_roots` decides only where it may *start*, gating the `Working directory:` path taken from the request's system prompt (client-controlled text) to canonicalized paths under roots you list. Keep the sandbox on and the bind on loopback. Prefer the `antigravity` provider above, which needs none of this. See the [providers guide](https://shunt.dev/guides/providers/).
**`antigravity-cli` is deprecated and is arbitrary code execution.** It runs the local `agy` binary in agentic mode: the CLI uses its own tools to do the work and shunt streams its progress back as Anthropic SSE. It can therefore never return a `tool_use` block, so a request that actually asks for one — a non-empty `tools` array, or a `tool_choice` of `any`/`tool` — is refused with a `400` rather than silently answered as text. `tool_choice: none` is exempt even alongside `tools`, and so is `auto` with no tools, since neither obliges a tool call. Because a non-interactive run cannot answer a permission prompt, `agy` runs with `--dangerously-skip-permissions`, so **treat this provider as arbitrary code execution as the user running shunt**. Two settings bound it: `sandbox` (default `true`) passes `--sandbox`, which keeps reads and writes inside the workspace and is what actually contains the agent; `workspace_roots` decides only where it may *start*, gating the `Working directory:` path taken from the request's system prompt (client-controlled text) to canonicalized paths under roots you list. Keep the sandbox on and the bind on loopback. Prefer the `antigravity` provider above, which needs none of this. See the [providers guide](https://shunt.dev/guides/providers/).

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 Badge Update the maintained README translations

This documents the new antigravity-cli 400 behavior only in README.md; README.ja.md, README.ko.md, and README.zh-CN.md were not updated, so users of the maintained translations will not know that requests advertising tools are now rejected. Apply the behavioral update to all three translated READMEs in the same change.

AGENTS.md reference: AGENTS.md:L57-L62

Useful? React with 👍 / 👎.

Comment on lines +83 to +85
- **Tools are `agy`'s, not the caller's, and a request asking for one is refused.** This
adapter can never emit a `tool_use` block, so a non-empty `tools` array, or a
`tool_choice` of `any` or `tool`, is rejected with a `400 invalid_request_error` naming

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 Badge Update the localized provider guides

The English providers guide now explains that antigravity-cli rejects these requests, but the maintained ja, ko, and zh-cn copies of guides/providers.mdx were left unchanged and do not contain this behavior, leaving the published locale guides stale. Add the equivalent section to each locale copy alongside this update.

AGENTS.md reference: AGENTS.md:L57-L64

Useful? React with 👍 / 👎.

@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.59701% with 13 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/adapters/cursor/agent.rs 80.59% 13 Missing ⚠️

📢 Thoughts on this report? Let us know!

@cubic-dev-ai cubic-dev-ai 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.

2 issues found across 9 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/adapters/antigravity/mod.rs">

<violation number="1" location="src/adapters/antigravity/mod.rs:612">
P3: The added doc block is inserted before the existing `agy_not_found` item, so the binary-lookup documentation now annotates `reject_caller_tools` and `agy_not_found` loses it. Move `agy_not_found` above the new helper, or move the new helper block below `agy_not_found` so each function keeps the correct documentation.</violation>
</file>

<file name="README.md">

<violation number="1" location="README.md:154">
P3: Update `README.ko.md`, `README.ja.md`, and `README.zh-CN.md` to document the new `400` rejection; otherwise localized users are still told that caller tools are ignored.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Client as Anthropic Client
    participant Adapter as Shunt Adapter Layer
    participant Cursor as Cursor Agent Service
    participant AGY as Antigravity CLI (agy)
    
    Note over Client,AGY: Request Validation Path (antigravity-cli)
    
    Client->>Adapter: Messages request with tools/tool_choice
    Adapter->>Adapter: NEW: Check tool_choice and tools fields
    
    alt tool_choice = none OR (no tools AND tool_choice = auto) OR empty tools
        Adapter->>AGY: Forward text-only prompt
        AGY-->>Adapter: Text response stream
        Adapter-->>Client: 200 text-only response (end_turn)
    else non-empty tools OR tool_choice = any/tool
        Adapter-->>Client: 400 invalid_request_error (reject)
        Note over Client,Adapter: Explicit failure instead of silent text-only 200
    end
    
    Note over Client,Cursor: Cursor Adapter - Built-in Tool Detection
    
    Client->>Adapter: Agentic request with caller-supplied tools
    Adapter->>Cursor: ConnectRPC agent request
    
    Cursor-->>Adapter: ExecServerMessage stream
    
    loop Per turn
        Cursor-->>Adapter: Frame (field 7 or 11)
        Adapter->>Adapter: NEW: Check field 11 (bridged MCP) first
        
        alt Field 11 contains tool call (bridged path)
            Adapter->>Adapter: Extract tool name + args
            Adapter-->>Client: Stream tool_use block
        else Field 7+ with tool_<uuid> id (built-in path)
            Adapter->>Adapter: NEW: Detect tool_ prefix outside field 11
            Adapter-->>Client: 502 error (explicit failure)
            Note over Adapter,Client: No silent end_turn - caller sees the failure
        else No tool ID present
            Adapter-->>Client: Stream text/reasoning content
        end
    end
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/adapters/cursor/agent.rs Outdated
Comment thread src/adapters/antigravity/mod.rs Outdated
/// which excludes `~/.local/bin` — the default install location for `agy` — so
/// a provider that works in a shell returns 503 under the service with no
/// indication why. `AGY_BIN` is the fix, and the message has to say so.
/// Reject a request that carries caller-supplied tools.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The added doc block is inserted before the existing agy_not_found item, so the binary-lookup documentation now annotates reject_caller_tools and agy_not_found loses it. Move agy_not_found above the new helper, or move the new helper block below agy_not_found so each function keeps the correct documentation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/adapters/antigravity/mod.rs, line 612:

<comment>The added doc block is inserted before the existing `agy_not_found` item, so the binary-lookup documentation now annotates `reject_caller_tools` and `agy_not_found` loses it. Move `agy_not_found` above the new helper, or move the new helper block below `agy_not_found` so each function keeps the correct documentation.</comment>

<file context>
@@ -608,6 +609,64 @@ fn terminal_failure(end: Option<&AgyEnd>, timed_out: bool, stderr: &StderrLog) -
 /// which excludes `~/.local/bin` — the default install location for `agy` — so
 /// a provider that works in a shell returns 503 under the service with no
 /// indication why. `AGY_BIN` is the fix, and the message has to say so.
+/// Reject a request that carries caller-supplied tools.
+///
+/// `agy` resolves its own tool calls internally and has no mode that hands them
</file context>

Comment thread src/adapters/cursor/agent.rs Outdated
Comment thread README.md Outdated
**Antigravity has two transports.** The `antigravity` provider talks to the Google Antigravity backend over HTTP, authenticated with `shunt login antigravity` — a Google authorization-code flow using Antigravity's own OAuth client and scopes, so a Gemini CLI login cannot be reused for it. It speaks the same Code Assist protocol as the `gemini` provider and currently serves the Gemini-family Antigravity models; the Claude models Antigravity also offers need request rewrites that are not implemented yet (#368).

**`antigravity-cli` is deprecated and is arbitrary code execution.** It runs the local `agy` binary in agentic mode: the CLI uses its own tools to do the work and shunt streams its progress back as Anthropic SSE. A `tools` array on the request is therefore not forwarded and no `tool_use` block is ever returned. Because a non-interactive run cannot answer a permission prompt, `agy` runs with `--dangerously-skip-permissions`, so **treat this provider as arbitrary code execution as the user running shunt**. Two settings bound it: `sandbox` (default `true`) passes `--sandbox`, which keeps reads and writes inside the workspace and is what actually contains the agent; `workspace_roots` decides only where it may *start*, gating the `Working directory:` path taken from the request's system prompt (client-controlled text) to canonicalized paths under roots you list. Keep the sandbox on and the bind on loopback. Prefer the `antigravity` provider above, which needs none of this. See the [providers guide](https://shunt.dev/guides/providers/).
**`antigravity-cli` is deprecated and is arbitrary code execution.** It runs the local `agy` binary in agentic mode: the CLI uses its own tools to do the work and shunt streams its progress back as Anthropic SSE. It can therefore never return a `tool_use` block, so a request that actually asks for one — a non-empty `tools` array, or a `tool_choice` of `any`/`tool` — is refused with a `400` rather than silently answered as text. `tool_choice: none` is exempt even alongside `tools`, and so is `auto` with no tools, since neither obliges a tool call. Because a non-interactive run cannot answer a permission prompt, `agy` runs with `--dangerously-skip-permissions`, so **treat this provider as arbitrary code execution as the user running shunt**. Two settings bound it: `sandbox` (default `true`) passes `--sandbox`, which keeps reads and writes inside the workspace and is what actually contains the agent; `workspace_roots` decides only where it may *start*, gating the `Working directory:` path taken from the request's system prompt (client-controlled text) to canonicalized paths under roots you list. Keep the sandbox on and the bind on loopback. Prefer the `antigravity` provider above, which needs none of this. See the [providers guide](https://shunt.dev/guides/providers/).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Update README.ko.md, README.ja.md, and README.zh-CN.md to document the new 400 rejection; otherwise localized users are still told that caller tools are ignored.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At README.md, line 154:

<comment>Update `README.ko.md`, `README.ja.md`, and `README.zh-CN.md` to document the new `400` rejection; otherwise localized users are still told that caller tools are ignored.</comment>

<file context>
@@ -151,7 +151,7 @@ xAI may gate OAuth access by subscription tier — if `grok` returns 403, use th
 **Antigravity has two transports.** The `antigravity` provider talks to the Google Antigravity backend over HTTP, authenticated with `shunt login antigravity` — a Google authorization-code flow using Antigravity's own OAuth client and scopes, so a Gemini CLI login cannot be reused for it. It speaks the same Code Assist protocol as the `gemini` provider and currently serves the Gemini-family Antigravity models; the Claude models Antigravity also offers need request rewrites that are not implemented yet (#368).
 
-**`antigravity-cli` is deprecated and is arbitrary code execution.** It runs the local `agy` binary in agentic mode: the CLI uses its own tools to do the work and shunt streams its progress back as Anthropic SSE. A `tools` array on the request is therefore not forwarded and no `tool_use` block is ever returned. Because a non-interactive run cannot answer a permission prompt, `agy` runs with `--dangerously-skip-permissions`, so **treat this provider as arbitrary code execution as the user running shunt**. Two settings bound it: `sandbox` (default `true`) passes `--sandbox`, which keeps reads and writes inside the workspace and is what actually contains the agent; `workspace_roots` decides only where it may *start*, gating the `Working directory:` path taken from the request's system prompt (client-controlled text) to canonicalized paths under roots you list. Keep the sandbox on and the bind on loopback. Prefer the `antigravity` provider above, which needs none of this. See the [providers guide](https://shunt.dev/guides/providers/).
+**`antigravity-cli` is deprecated and is arbitrary code execution.** It runs the local `agy` binary in agentic mode: the CLI uses its own tools to do the work and shunt streams its progress back as Anthropic SSE. It can therefore never return a `tool_use` block, so a request that actually asks for one — a non-empty `tools` array, or a `tool_choice` of `any`/`tool` — is refused with a `400` rather than silently answered as text. `tool_choice: none` is exempt even alongside `tools`, and so is `auto` with no tools, since neither obliges a tool call. Because a non-interactive run cannot answer a permission prompt, `agy` runs with `--dangerously-skip-permissions`, so **treat this provider as arbitrary code execution as the user running shunt**. Two settings bound it: `sandbox` (default `true`) passes `--sandbox`, which keeps reads and writes inside the workspace and is what actually contains the agent; `workspace_roots` decides only where it may *start*, gating the `Working directory:` path taken from the request's system prompt (client-controlled text) to canonicalized paths under roots you list. Keep the sandbox on and the bind on loopback. Prefer the `antigravity` provider above, which needs none of this. See the [providers guide](https://shunt.dev/guides/providers/).
 
 **Migrating from the old `antigravity`.** `kind = "antigravity"` used to mean the local CLI. A config still carrying that meaning is refused by name rather than silently retargeted, and a routed `antigravity` provider with no credential refuses to start — switching transport, credentials, and egress underneath a green startup would be worse than failing. Run `shunt login antigravity`, or point the route at `antigravity-cli`.
</file context>

@codspeed-hq

codspeed-hq Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 83 untouched benchmarks


Comparing r-uben:fix/cursor-builtin-tool-call-dropped (cb364f8) with main (3c1d673)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (fe35207) during the generation of this report, so 3c1d673 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

Composer chooses per turn whether to invoke a bridged caller tool or one of
Cursor's own built-in tools. A bridged call arrives as
`ExecServerMessage.mcp_args` (field 11) and decodes correctly. A built-in call
arrives under a tool-specific field carrying only its arguments and a
`tool_<uuid>` call id, with no tool name -- observed for the built-in file read
as `ExecServerMessage` field 7: `{1: "<path>", 2: "tool_<uuid>"}`.

`extract_tool_call` matched field 11 only, so a built-in call fell through to
the `End` branch: the turn closed with `end_turn` and no `tool_use`, and the
model appeared to announce a tool call and then do nothing. Captured against
the live upstream over six turns, the correlation was exact -- every failing
turn carried field 7 and no field 11, every passing turn the reverse.

shunt cannot bridge these: the caller owns the filesystem, and the frame
carries no name to map onto a caller tool. Detect them via the `tool_` call-id
prefix outside field 11 (across captured turns only fields 7 and 11 ever carry
one) and fail the turn with an explicit error, turning a silent truncation into
a visible one. Verified over ten live turns: five bridged tool calls, five
explicit errors, zero silent `end_turn`s.
@r-uben
r-uben force-pushed the fix/cursor-builtin-tool-call-dropped branch from 6f432ea to 8970e7e Compare August 20, 2026 17:36

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8970e7e4ec

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread site/src/content/docs/providers/cursor.mdx
Review feedback: the message relied on trailing whitespace before a line
continuation, so whitespace cleanup could join `tool,` and `which` into a
malformed string.

Also record why the call-id scan stays wide. Two narrower variants were built
and measured against the live upstream on an identical request: pinning
detection to the captured field position leaked silent turns (2 of 22), and
additionally requiring a uuid-shaped id body leaked badly (8 of 16), even
though every captured id satisfies that shape. The wide scan showed no silent
turn in 38.
@r-uben

r-uben commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — addressed the concat! nit in 988ff3e.

On the P2 suggestion (restrict detection to call-ID field positions rather than scanning nested strings): I built it and measured it, and it makes things worse, so I've kept the wide scan and documented why in the code.

Two narrower variants, each A/B'd against the live upstream on an identical tool-bearing request, counting turns that returned neither a tool_use nor an explicit error (i.e. the original silent truncation):

detector silent turns
wide scan (this PR) 0 of 38
pinned to the captured field position 2 of 22
pinned position + uuid-shaped id body 8 of 16

The uuid-shape variant is the surprising one: all six call ids captured from real frames satisfy that shape (tool_ + 35 chars of hex and dashes), yet requiring it still leaked half the turns. So the id evidently reaches the detector in a form my reconstruction doesn't capture, and narrowing on the captured shape is not as safe as it reads.

The tradeoff isn't symmetric either way, but it is a tradeoff: a false positive fails a turn that was working, while a miss just restores the pre-existing silent truncation. I agree the prefix check is loose in principle — I'd happily replace the whole heuristic with an explicit field allowlist if anyone has the real ExecServerMessage descriptor. Absent that, the measurements are what I have.

Re the README translation gap and the agy_not_found doc-block placement: both belong to the Antigravity commits that were accidentally bundled into this PR. It has since been rebased onto main and now contains only the Cursor change, so those two findings no longer apply here.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 988ff3ed88

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread site/src/content/docs/providers/cursor.mdx Outdated

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 336a124ca5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread site/src/content/docs/providers/cursor.mdx
- match the `tool_` call-id prefix on raw bytes instead of running a full
  UTF-8 validation over every nested field of every streamed frame
- drop the "or send a request with no tools" remedy from the 502 message and
  the matching claim from the provider page: `ReadState` carries no
  request-tool context, so detection fires regardless of what was advertised
- correct the `MAX_TOOL_ID_SCAN_DEPTH` doc, which claimed to mirror a
  constant that is 64
- cover the byte-prefix semantics with a call id that has a non-UTF-8 tail
@amondnet

Copy link
Copy Markdown
Contributor

/gemini review

@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 adds detection and explicit error handling for unbridged built-in tool calls from Cursor, along with corresponding documentation and tests. A high-severity issue was identified in the recursive scanning of protobuf fields, which could cause false positives on arbitrary user content (such as file paths or contents) that happens to match the protobuf wire format and contain the 'tool_' prefix. It is recommended to restrict the scan to direct fields of the tool message to prevent these false positives.

Comment thread src/adapters/cursor/agent.rs
The README described the Cursor adapter as bridging caller tools with no
mention that a turn can now fail when Composer answers through one of
Cursor's own built-in tools instead.

The ko/ja/zh-CN READMEs need no matching change: their Cursor paragraphs
never carried the tool-bridging claim — they abbreviate to the prefix/suffix
rule and defer to the providers page.
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.

2 participants