Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/adapters/cursor/native-exec-fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ function codexNativeMutationRefusal(operation: "write" | "delete", structuredEdi
}

const NATIVE_LOCAL_EXEC_DISABLED =
"Route filesystem work through the Codex Responses bridge shell tool (`shell_command` / `exec_command`, or the listed `mcp_opencodex-responses_*` display alias) with equivalent shell commands (cat, head, ls, rg, grep), or `apply_patch` for file edits. Do not tell the user shell or read access is blocked, disabled, or denied — silently call that bridge tool.";
"Cursor-native filesystem tools are not executed locally. Use a catalog tool for this work: `shell_command` / `exec_command` (or the listed `mcp_opencodex-responses_*` display alias) with host-shell-safe equivalents: POSIX (`cat`, `head`, `ls`, `rg`, `grep`) or Windows PowerShell (`Get-Content`, `Get-ChildItem`, `Select-String`); use `apply_patch` for file edits.";

export function rejectReadExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
if (execMsg.message.case !== "readArgs") throw new Error("invalid read exec");
Expand Down
2 changes: 1 addition & 1 deletion src/adapters/cursor/native-exec-network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export interface CursorNativeNetworkDeps {
}

const NATIVE_FETCH_DISABLED =
"Route this through the Codex shell bridge tool `shell_command` (aliases: `exec_command`, `mcp_opencodex-responses_shell_command`, `mcp_opencodex-responses_exec_command`) with curl or wget. Do not tell the user network or shell access is blocked, disabled, or denied — silently call that bridge tool.";
"Cursor-native fetch is not executed locally. Use the Codex shell bridge tool `shell_command` (aliases: `exec_command`, `mcp_opencodex-responses_shell_command`, `mcp_opencodex-responses_exec_command`) with curl or wget.";

export function rejectFetchExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
if (execMsg.message.case !== "fetchArgs") throw new Error("invalid fetch exec");
Expand Down
1 change: 0 additions & 1 deletion src/adapters/cursor/native-exec-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ export function nativeShellDisabledMessage(): string {
// client that executes the bridge (LAN/SSH remote-proxy).
return (
"Route this through the Codex bridge shell tool from the current catalog (`shell_command` or `exec_command`, including the long `mcp_opencodex-responses_*` display name if listed). "
+ "Do not tell the user shell access is blocked, disabled, or denied — silently call that bridge tool. "
+ "Adapt the command for the Codex client host shell before calling the bridge "
+ "(Windows PowerShell 5.1: no CMD `cd /d`, no bash heredocs; `&&`/`||` are unsupported parser errors — prefer the bridge working-directory argument for directory changes, and use `if ($?) { ... }` for success-gated follow-up steps; do not treat `;` as a substitute for `&&`). "
+ "Make at most one corrected bridge attempt after a failure, then report the error and stop — do not repeat equivalent failing commands."
Expand Down
15 changes: 9 additions & 6 deletions src/adapters/cursor/protobuf-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ function rootBlobCandidate(
};
}

function toolResultRootPayload(text: string): { role: "assistant"; content: [{ type: "text"; text: string }] } {
return { role: "assistant", content: [{ type: "text", text }] };
}

function truncateToolResultBlob(entry: RootBlobCandidate, maxBytes: number): RootBlobCandidate | null {
if (entry.byteLength <= maxBytes) return entry;
if (entry.role !== "toolResult" || entry.text === undefined) return null;
Expand All @@ -134,7 +138,7 @@ function truncateToolResultBlob(entry: RootBlobCandidate, maxBytes: number): Roo
while (end > 0 && end < encoded.byteLength && (encoded[end]! & 0xc0) === 0x80) end -= 1;
const truncated = `${decoder.decode(encoded.subarray(0, end))}${marker}`;
const result = rootBlobCandidate(
{ role: "user", content: [{ type: "text", text: truncated }] },
toolResultRootPayload(truncated),
"toolResult",
{ messageIndex: entry.messageIndex, text: truncated },
);
Expand All @@ -143,7 +147,7 @@ function truncateToolResultBlob(entry: RootBlobCandidate, maxBytes: number): Roo
keepBytes = Math.max(0, end - (result.byteLength - maxBytes) - 16);
}
const markerOnly = rootBlobCandidate(
{ role: "user", content: [{ type: "text", text: marker.trimStart() }] },
toolResultRootPayload(marker.trimStart()),
"toolResult",
{ messageIndex: entry.messageIndex, text: marker.trimStart() },
);
Expand Down Expand Up @@ -175,9 +179,8 @@ function assistantRootText(
// Cursor builds the actual model prompt from rootPromptMessagesJson (turns[] is UI/display metadata),
// so prior history — including assistant tool calls and tool results — must be replayed here or a
// ResumeAction has nothing model-visible to continue from. The active user message is excluded
// because it travels in the action. Tool results are rendered as user-role text with a marker, and
// each entry is a SHA-256 blob ID (Cursor fetches the bytes back via getBlobArgs). Mirrors the
// danger-pi reference buildRootPromptMessagesJson.
// because it travels in the action. Tool results are assistant-role text with a [Tool Result]
// or [Tool Error] marker so Cursor does not wrap them as `<user_query>` (#1992). Each entry is a SHA-256 blob ID.
function rootPromptMessages(request: CursorRunRequest, requestScope: CursorBlobRequestScopeToken): {
ids: Uint8Array[];
byteLength: number;
Expand Down Expand Up @@ -232,7 +235,7 @@ function rootPromptMessages(request: CursorRunRequest, requestScope: CursorBlobR
const prefix = message.isError ? "[Tool Error]" : "[Tool Result]";
const text = `${prefix}\n${toolResultToText(message)}`;
entries.push(rootBlobCandidate(
{ role: "user", content: [{ type: "text", text }] },
toolResultRootPayload(text),
"toolResult",
{ messageIndex: i, text },
));
Expand Down
25 changes: 18 additions & 7 deletions src/adapters/cursor/tool-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,15 @@ export const CURSOR_STRUCTURED_EDIT_TOOLS = [CURSOR_EDIT_FILE_TOOL, CURSOR_MULTI
export const CURSOR_EXEC_COMMAND_TOOL = CODEX_EXEC_COMMAND_TOOL;
export const CODEX_SHELL_BRIDGE_TOOL_NAMES = [CODEX_EXEC_COMMAND_TOOL, CODEX_SHELL_COMMAND_TOOL] as const;
export const CURSOR_SHELL_ALIAS_SYSTEM_NOTE =
'Shell commands use the Codex shell bridge tool shown in this turn\'s catalog (`shell_command` or `exec_command`) with JSON arguments like {"cmd":"..."}. The long `mcp_opencodex-responses_*` display name is the same tool. Prefer it over Cursor-native Shell; never say native shell is blocked.';
'Shell commands use the Codex shell bridge tool shown in this turn\'s catalog (`shell_command` or `exec_command`) with JSON arguments like {"cmd":"..."}. The long `mcp_opencodex-responses_*` display name is the same tool. Prefer it over Cursor-native Shell.';
const NEIGHBOR_AGENT_TOOL_NAMES = ["Read", "Grep", "Glob", "Bash", "LS"] as const;
const NEIGHBOR_AGENT_TOOL_ALIASES: Record<(typeof NEIGHBOR_AGENT_TOOL_NAMES)[number], readonly string[]> = {
Read: ["read", "read_file"],
Grep: ["grep"],
Glob: ["glob", "find"],
Bash: ["bash", "shell"],
LS: ["ls"],
};

export const CURSOR_GENERIC_TOOL_USE_USER_HINT = [
"For generic tool-use/count demos, satisfy the request with repeated Codex shell bridge calls (`shell_command` or `exec_command`) for harmless commands.",
Expand All @@ -29,8 +36,7 @@ export const CURSOR_GENERIC_TOOL_USE_USER_HINT = [
"The Cursor bridge may suspend after the first returned bridge tool call, so emit sibling calls together before any result is needed.",
"If parallel emission is unavailable, continue with separate shell-bridge calls until the requested count has returned.",
"Do not use `tool_search`, external MCP, or resource discovery just to pad the count unless explicitly asked.",
"Do not suggest or switch to neighboring-agent tools such as `Grep`, `Read`, `Glob`, `Bash`, or `LS` unless this turn's catalog lists those exact names.",
"Never tell the user that shell or read access is blocked, disabled, or denied unless the Codex shell bridge tool itself fails. Do not narrate Cursor-native Shell/Read routing.",
"Do not suggest or switch to neighboring-agent tools such as `Grep`, `Read`, `Glob`, `Bash`, or `LS` unless this turn's catalog lists those exact names or an equivalent listed client tool.",
].join(" ");

export const CURSOR_EXEC_COMMAND_INPUT_SCHEMA = {
Expand Down Expand Up @@ -563,9 +569,14 @@ function quotedNames(names: readonly string[]): string {
return names.map(name => `\`${name}\``).join(", ");
}

function advertisedCoversNeighbor(wireNames: readonly string[], neighbor: (typeof NEIGHBOR_AGENT_TOOL_NAMES)[number]): boolean {
const advertised = new Set(wireNames.map(name => name.toLowerCase()));
if (advertised.has(neighbor.toLowerCase())) return true;
return NEIGHBOR_AGENT_TOOL_ALIASES[neighbor].some(alias => advertised.has(alias.toLowerCase()));
}

function unavailableNeighborAgentToolNames(wireNames: readonly string[]): string[] {
const advertised = new Set(wireNames);
return NEIGHBOR_AGENT_TOOL_NAMES.filter(name => !advertised.has(name));
return NEIGHBOR_AGENT_TOOL_NAMES.filter(name => !advertisedCoversNeighbor(wireNames, name));
}

function discoveryToolLabel(wireNames: readonly string[]): string | undefined {
Expand Down Expand Up @@ -632,7 +643,7 @@ export function buildCursorToolGuidanceSystemNote(
? "Your tool list may display it under a longer `mcp_opencodex-responses_shell_command` / `mcp_opencodex-responses_exec_command` name; those are the SAME tool — call whichever your list shows, and do not comment on the naming difference to the user."
: undefined,
hasBareExec
? "Never tell the user that shell or read access is blocked, disabled, or denied unless the Codex shell bridge tool itself fails. Prefer the bridge over Cursor-native Shell/Read; do not narrate phrases like \"Native shell access is blocked\" — silently call `shell_command` / `exec_command`."
? `Prefer the Codex shell bridge over Cursor-native Shell/Read. If a Cursor-native file read, directory listing, grep, or shell operation is rejected, continue with the listed catalog tool ${shellBridgeLabel}.`
: undefined,
hostShellNote,
"Cursor product features (Chronicle, screen recording, Notes, Plans, background agents) are available only if this turn's catalog lists a matching tool; do not offer or promise them otherwise.",
Expand All @@ -656,7 +667,7 @@ export function buildCursorToolGuidanceSystemNote(
: undefined,
"Do not count or report a tool call unless a tool result was actually returned.",
hasBareExec
? `If a Cursor-native file read, directory listing, grep, or shell operation is rejected by the runtime, silently use ${shellBridgeLabel} with an equivalent host-shell-safe command (POSIX: \`cat\`/\`ls\`/\`rg\`; Windows PowerShell: \`Get-Content\`/\`Get-ChildItem\`/\`Select-String\`). Do not tell the user access is blocked. For file edits, use ${structuredEditNames.length > 0 ? `the structured edit tools (${quotedNames(structuredEditNames)}) or ` : ""}\`apply_patch\` when available.`
? `If a Cursor-native file read, directory listing, grep, or shell operation is rejected by the runtime, use ${shellBridgeLabel} with an equivalent host-shell-safe command (POSIX: \`cat\`/\`ls\`/\`rg\`; Windows PowerShell: \`Get-Content\`/\`Get-ChildItem\`/\`Select-String\`). For file edits, use ${structuredEditNames.length > 0 ? `the structured edit tools (${quotedNames(structuredEditNames)}) or ` : ""}\`apply_patch\` when available.`
: undefined,
].filter((note): note is string => typeof note === "string");
return notes.join(" ");
Expand Down
2 changes: 2 additions & 0 deletions tests/cursor-blob.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,8 @@ describe("Cursor blob handshake", () => {
const roots = decodeRootMessages(bytes) as Array<{ role?: string; content?: unknown }>;
const historicalUser = roots.find(root => root.role === "user");
expect(historicalUser?.content).toEqual([{ type: "text", text: "read a file" }]);
const toolResultRoot = roots.find(root => JSON.stringify(root).includes("[Tool Result]"));
expect(toolResultRoot?.role).toBe("assistant");
expect(run?.action?.action.case).toBe("resumeAction");
expect(JSON.stringify(roots)).toContain("contents");
expect(JSON.stringify(roots)).not.toContain("hidden reasoning");
Expand Down
14 changes: 9 additions & 5 deletions tests/cursor-native-exec-policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,12 @@ describe("Cursor native exec sandbox policy", () => {
expect(deniedText).toContain("exec_command");
expect(deniedText).toContain("mcp_opencodex-responses_*");
expect(deniedText).toContain("cat");
expect(deniedText).toContain("Get-Content");
expect(deniedText).toContain("Get-ChildItem");
expect(deniedText).toContain("Select-String");
expect(deniedText).toContain("apply_patch");
expect(deniedText).toContain("silently call");
expect(deniedText).toContain("Do not tell the user");
expect(deniedText).not.toContain("silently call");
expect(deniedText).not.toContain("Do not tell the user");
expect(deniedText).not.toContain("disabled by OpenCodex policy");
expect(deniedText).not.toContain("sandbox denial");
expect(deniedText).not.toContain(content);
Expand All @@ -146,11 +149,11 @@ describe("Cursor native exec sandbox policy", () => {
value: create(ShellArgsSchema, { command: "printf SHOULD_NOT_RUN", workingDirectory: dir, hardTimeout: 2000 }),
}), { unsafeAllowNativeLocalExec }))[0]);
const deniedShellText = stringify(deniedShell);
expect(deniedShellText).toContain("silently call");
expect(deniedShellText).not.toContain("silently call");
expect(deniedShellText).toContain("shell_command");
expect(deniedShellText).toContain("exec_command");
expect(deniedShellText).toContain("mcp_opencodex-responses_*");
expect(deniedShellText).toContain("Do not tell the user");
expect(deniedShellText).not.toContain("Do not tell the user");
expect(deniedShellText).not.toContain("with the same command");
expect(deniedShellText).toContain("at most one corrected bridge attempt");
expect(deniedShellText).toContain("if ($?)");
Expand Down Expand Up @@ -178,7 +181,8 @@ describe("Cursor native exec sandbox policy", () => {
}))[0]);
expect(fetchCalled).toBe(false);
const deniedFetchText = stringify(deniedFetch);
expect(deniedFetchText).toContain("silently call");
expect(deniedFetchText).not.toContain("silently call");
expect(deniedFetchText).not.toContain("Do not tell the user");
expect(deniedFetchText).toContain("shell_command");
expect(deniedFetchText).toContain("curl");
expect(deniedFetchText).toContain("wget");
Expand Down
20 changes: 13 additions & 7 deletions tests/cursor-native-exec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,12 @@ describe("Cursor native exec bridge", () => {
expect(deniedRead.message.value.result.value.error).toContain("shell_command");
expect(deniedRead.message.value.result.value.error).toContain("exec_command");
expect(deniedRead.message.value.result.value.error).toContain("cat");
expect(deniedRead.message.value.result.value.error).toContain("Get-Content");
expect(deniedRead.message.value.result.value.error).toContain("Get-ChildItem");
expect(deniedRead.message.value.result.value.error).toContain("Select-String");
expect(deniedRead.message.value.result.value.error).toContain("apply_patch");
expect(deniedRead.message.value.result.value.error).toContain("silently call");
expect(deniedRead.message.value.result.value.error).toContain("Do not tell the user");
expect(deniedRead.message.value.result.value.error).not.toContain("silently call");
expect(deniedRead.message.value.result.value.error).not.toContain("Do not tell the user");
expect(deniedRead.message.value.result.value.error).not.toContain("disabled by OpenCodex policy");
expect(deniedRead.message.value.result.value.error).not.toContain("sandbox denial");
}
Expand All @@ -133,7 +136,8 @@ describe("Cursor native exec bridge", () => {
expect(deniedShell.message.value.result.value.stderr).toContain("shell_command");
expect(deniedShell.message.value.result.value.stderr).toContain("exec_command");
expect(deniedShell.message.value.result.value.stderr).toContain("mcp_opencodex-responses_*");
expect(deniedShell.message.value.result.value.stderr).toContain("Do not tell the user");
expect(deniedShell.message.value.result.value.stderr).not.toContain("Do not tell the user");
expect(deniedShell.message.value.result.value.stderr).not.toContain("silently call");
expect(deniedShell.message.value.result.value.stderr).not.toContain("disabled by OpenCodex policy");
expect(deniedShell.message.value.result.value.stderr).not.toContain("sandbox denial");
}
Expand All @@ -150,7 +154,8 @@ describe("Cursor native exec bridge", () => {
expect(streamText).toContain("shell_command");
expect(streamText).toContain("exec_command");
expect(streamText).toContain("mcp_opencodex-responses_*");
expect(streamText).toContain("Do not tell the user");
expect(streamText).not.toContain("Do not tell the user");
expect(streamText).not.toContain("silently call");
expect(streamText).not.toContain("sandbox denial");

const deniedBackground = decode((await handleCursorNativeExec(execMessage({
Expand All @@ -162,7 +167,7 @@ describe("Cursor native exec bridge", () => {
if (deniedBackground.message.value.result.case === "error") {
expect(deniedBackground.message.value.result.value.error).toContain("shell_command");
expect(deniedBackground.message.value.result.value.error).toContain("exec_command");
expect(deniedBackground.message.value.result.value.error).toContain("Do not tell the user");
expect(deniedBackground.message.value.result.value.error).not.toContain("Do not tell the user");
}

const deniedStdin = decode((await handleCursorNativeExec(execMessage({
Expand All @@ -183,7 +188,8 @@ describe("Cursor native exec bridge", () => {
expect(deniedFetch.message.case).toBe("fetchResult");
expect(deniedFetch.message.value.result.case).toBe("error");
if (deniedFetch.message.value.result.case === "error") {
expect(deniedFetch.message.value.result.value.error).toContain("silently call");
expect(deniedFetch.message.value.result.value.error).not.toContain("silently call");
expect(deniedFetch.message.value.result.value.error).not.toContain("Do not tell the user");
expect(deniedFetch.message.value.result.value.error).toContain("shell_command");
expect(deniedFetch.message.value.result.value.error).toContain("curl");
expect(deniedFetch.message.value.result.value.error).toContain("wget");
Expand Down Expand Up @@ -228,7 +234,7 @@ describe("Cursor native exec bridge", () => {
expect(shell.message.value.result.value.stderr).toContain("shell_command");
expect(shell.message.value.result.value.stderr).toContain("exec_command");
expect(shell.message.value.result.value.stderr).toContain("mcp_opencodex-responses_*");
expect(shell.message.value.result.value.stderr).toContain("Do not tell the user");
expect(shell.message.value.result.value.stderr).not.toContain("Do not tell the user");
expect(shell.message.value.result.value.stderr).not.toContain("sandbox denial");
}
});
Expand Down
Loading
Loading