Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/adapters/cursor/tool-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,12 @@ export function cursorRequestHasShellAlias(tools: readonly Pick<OcxTool, "namesp
return tools?.some(isBareCodexExecCommandTool) ?? false;
}

function cursorRequestHasExecutionPath(
tools: readonly Pick<OcxTool, "namespace" | "name">[] | undefined,
): boolean {
return tools?.some(isCursorExecutionPathTool) ?? false;
}

export function cursorRequestAdvertisesApplyPatch(
tools: readonly Pick<OcxTool, "namespace" | "name" | "freeform">[] | undefined,
toolChoice?: OcxRequestOptions["toolChoice"],
Expand Down Expand Up @@ -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);
}
Expand All @@ -432,7 +438,7 @@ export function cursorToolsForActivePrompt<T extends Pick<OcxTool, "namespace" |
toolChoice?: OcxRequestOptions["toolChoice"],
): readonly T[] | undefined {
if (!shouldUseNativeExecOnlyForGenericToolUse(tools, activeText)) return tools;
const execTools = tools?.filter(isBareCodexExecCommandTool);
const execTools = tools?.filter(isCursorExecutionPathTool);
const catalog = tools ?? [];
if (execTools?.length && !execTools.some(tool => cursorToolAllowedByChoice(tool, toolChoice, catalog))) return tools;
return execTools && execTools.length > 0 ? execTools : tools;
Expand Down
14 changes: 14 additions & 0 deletions structure/04_transports-and-sidecars.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
19 changes: 19 additions & 0 deletions tests/cursor-blob.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
18 changes: 18 additions & 0 deletions tests/cursor-tool-continuation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
19 changes: 19 additions & 0 deletions tests/cursor-tool-definitions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {} },
Expand Down
Loading