diff --git a/src/adapters/cursor/tool-definitions.ts b/src/adapters/cursor/tool-definitions.ts index 7bdc544eb2..8a346040b2 100644 --- a/src/adapters/cursor/tool-definitions.ts +++ b/src/adapters/cursor/tool-definitions.ts @@ -200,6 +200,12 @@ export function cursorRequestHasShellAlias(tools: readonly Pick[] | undefined, +): boolean { + return tools?.some(isCursorExecutionPathTool) ?? false; +} + export function cursorRequestAdvertisesApplyPatch( tools: readonly Pick[] | undefined, toolChoice?: OcxRequestOptions["toolChoice"], @@ -421,7 +427,7 @@ export function shouldUseNativeExecOnlyForGenericToolUse( text: string, ): boolean { const trimmed = text.trim(); - if (trimmed.length === 0 || !cursorRequestHasShellAlias(tools) || !isGenericToolUseCountDemoPrompt(trimmed)) return false; + if (trimmed.length === 0 || !cursorRequestHasExecutionPath(tools) || !isGenericToolUseCountDemoPrompt(trimmed)) return false; return !/\b(?:mcp|resource|resources|tool_search|plugin|plugins|app connector|github)\b/i.test(trimmed) && !/(?:리소스|플러그인|깃허브|github)/i.test(trimmed); } @@ -432,7 +438,7 @@ export function cursorToolsForActivePrompt cursorToolAllowedByChoice(tool, toolChoice, catalog))) return tools; return execTools && execTools.length > 0 ? execTools : tools; diff --git a/structure/04_transports-and-sidecars.md b/structure/04_transports-and-sidecars.md index 5f1b6447e0..257ebf5b99 100644 --- a/structure/04_transports-and-sidecars.md +++ b/structure/04_transports-and-sidecars.md @@ -228,6 +228,20 @@ MCP, screen recording, and computer-use stay on their separate explicit executor - 다른 대안 대신 이 방식을 선택한 이유: opencodex has no trustworthy per-request sandbox attestation in request text or headers, so any prompt-carried marker is spoofable by data-plane callers. - 장점, 단점 및 영향: this closes prompt-to-native-exec escalation while preserving an explicit operator escape hatch; existing configs that relied on `codex-sandbox` must switch to `nativeLocalExec: "on"` for trusted local experiments. +Cursor's generic tool-use prompt filter must preserve every Responses-owned execution-path tool +that survives the transport budget: unified Desktop `exec` as well as the legacy +`exec_command`/`shell_command` aliases. The legacy aliases receive Cursor-specific shell guidance; +unified `exec` keeps its own schema and is surfaced back to Codex as a client tool. It must never +fall through to the separate native-local-exec dispatcher. + +[Decision Log] +- 목적과 의도: keep fresh Cursor-routed Codex Desktop subagents able to invoke the actual unified `exec` tool exposed by their client catalog. +- 기존 구현 및 제약 조건: catalog truncation already pinned `exec`, but the later generic-tool filter recognized only bare `exec_command`/`shell_command` and could erase the sole executable client tool while also naming aliases that were absent. +- 검토한 주요 대안: synthesize a legacy alias, execute `exec` through Cursor native-local-exec, disable generic filtering, or treat every Responses-owned execution-path tool as eligible. +- 선택한 방식: preserve the existing client tool and schema by filtering with `isCursorExecutionPathTool`; keep alias-specific prompt guidance gated on an alias actually being present. +- 다른 대안 대신 이 방식을 선택한 이유: Codex Desktop remains the execution and approval authority, no unavailable tool name is invented, and the existing Responses MCP suspension path can relay the call without widening native execution privileges. +- 장점, 단점 및 영향: unified `exec` survives the filter and returns to Desktop for execution; legacy aliases behave as before; `wait` and unrelated tools remain excluded from generic tool-count prompts. + ## WebSocket The WebSocket endpoint exists at `/v1/responses`, but discovery is opt-in: diff --git a/tests/cursor-blob.test.ts b/tests/cursor-blob.test.ts index 509b1d3361..e8df19da3e 100644 --- a/tests/cursor-blob.test.ts +++ b/tests/cursor-blob.test.ts @@ -650,6 +650,25 @@ describe("Cursor AgentRunRequest.mcp_tools channel", () => { expect(mcpToolNames(bytes)).toEqual(["exec_command"]); }); + test("mcp_tools keeps unified Desktop exec for a generic tool-use prompt", () => { + const bytes = encodeCursorRunRequest({ + modelId: "gpt-5.6-luna-high", + conversationId: "c1", + system: ["You are helpful."], + messages: [{ role: "user", content: "use any 3 tools" }], + tools: [ + { + name: "exec", + description: "Run a command", + parameters: { type: "object", properties: { cmd: { type: "string" } }, required: ["cmd"] }, + }, + { name: "wait", description: "Wait for a yielded cell", parameters: {} }, + { name: "js", namespace: "mcp__node_repl", description: "Run JS", parameters: {} }, + ], + }); + expect(mcpToolNames(bytes)).toEqual(["exec"]); + }); + test("leaves mcp_tools unset when tools are empty", () => { const bytes = encodeCursorRunRequest({ modelId: "gpt-5.6-luna-high", diff --git a/tests/cursor-tool-continuation.test.ts b/tests/cursor-tool-continuation.test.ts index f5d8948520..6880d741cb 100644 --- a/tests/cursor-tool-continuation.test.ts +++ b/tests/cursor-tool-continuation.test.ts @@ -136,6 +136,24 @@ describe("363-A: turn-1 termination for Responses client tool via exec mcpArgs", expect(finalized.map(e => e.type)).toEqual(["done"]); }); + test("unified Desktop exec is surfaced as a Responses client tool instead of native-exec fallback", () => { + const state = createCursorProtobufEventState({ clientToolNames: ["exec"] }); + const plan = planMcpArgsHandling(execMcpArgs({ + toolName: "exec", + toolCallId: "call_exec", + args: { cmd: new TextEncoder().encode(JSON.stringify("pwd")) }, + }), state); + + expect(plan.handledByResponsesBridge).toBe(true); + expect(plan.events).toEqual([ + { type: "tool_call_start", id: "call_exec", name: "exec" }, + { type: "tool_call_delta", arguments: "{\"cmd\":\"pwd\"}" }, + { type: "tool_call_end", id: "call_exec" }, + ]); + expect(plan.writeMcpResult).toBeUndefined(); + expect(plan.finalizeWhenDrained).toBe(true); + }); + test("no-checkpoint client-tool finalize carries forward the last known active context usage", () => { const tracker = createCursorContextUsageTracker(); tracker.record("cursor_conv_1", 183_336); diff --git a/tests/cursor-tool-definitions.test.ts b/tests/cursor-tool-definitions.test.ts index d418b304b2..638f453097 100644 --- a/tests/cursor-tool-definitions.test.ts +++ b/tests/cursor-tool-definitions.test.ts @@ -259,6 +259,25 @@ describe("Cursor tool definitions", () => { expect(cursorToolsForActivePrompt(tools, "Use any 10 tools")?.map(tool => cursorToolWireName(tool))).toEqual(["shell_command"]); }); + test("preserves unified Desktop exec for generic tool-use without inventing shell aliases", () => { + for (const namespace of [undefined, "opencodex-responses"]) { + const tools: OcxTool[] = [ + { + name: "exec", + ...(namespace ? { namespace } : {}), + description: "Run a command", + parameters: { type: "object", properties: { cmd: { type: "string" } }, required: ["cmd"] }, + }, + { name: "wait", ...(namespace ? { namespace } : {}), description: "Wait", parameters: {} }, + { name: "tool_search", description: "Search tools", parameters: {} }, + ]; + + const visible = cursorToolsForActivePrompt(tools, "Use any 3 tools"); + expect(visible?.map(tool => tool.name)).toEqual(["exec"]); + expect(appendCursorGenericToolUseHint(tools, "Use any 3 tools")).toBe("Use any 3 tools"); + } + }); + test("does not erase explicit non-exec tool_choice for generic tool-count prompts", () => { const tools: OcxTool[] = [ { name: "exec_command", description: "Run", parameters: {} },