Skip to content
Draft
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: 2 additions & 0 deletions src/adapters/cursor/live-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import { desktopDepsFromConfig } from "./native-exec-desktop";
import {
buildCursorToolDefinitions,
cursorRequestAdvertisesApplyPatch,
cursorRequestUsesCodeMode,
cursorRequestHasShellAlias,
cursorToolArgNormalizeSchema,
cursorToolWireName,
Expand Down Expand Up @@ -557,6 +558,7 @@ class LiveCursorTransport implements CursorTransport {
this.execContext = {
...this.execContext,
clientToolDefs,
codeMode: cursorRequestUsesCodeMode(request.tools, request.toolChoice),
rejectNativeFileMutations: cursorRequestAdvertisesApplyPatch(request.tools, request.toolChoice),
structuredEditAvailable: syntheticStructuredEditToolNames.size > 0,
};
Expand Down
38 changes: 23 additions & 15 deletions src/adapters/cursor/native-exec-fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,16 @@ import {
type GrepArgs,
} from "./gen/agent_pb";
import { errorText, execBytes, lineCount, textDecoder, textEncoder } from "./native-exec-common";
import { codeModeBridgeGuidance } from "./native-exec-guidance";

const MAX_GREP_FILES = 500;
const MAX_GREP_RESULTS = 200;
const MAX_FILE_BYTES = 1_000_000;

function codexNativeMutationRefusal(operation: "write" | "delete", structuredEditAvailable: boolean): string {
function codexNativeMutationRefusal(operation: "write" | "delete", structuredEditAvailable: boolean, codeMode?: boolean): string {
if (codeMode === true) {
return `Cursor-native ${operation} is disabled for this Codex request because file mutations must stay on the Codex approval path. Call the top-level \`exec\` tool with JavaScript in its body, then call \`await tools.apply_patch(...)\` inside that body and emit the result with \`text(...)\`. Do not call \`apply_patch\` as a top-level tool in code mode. No file was changed.`;
}
const structuredHint = structuredEditAvailable
? " Use the structured edit tools (`edit_file` / `multi_edit`) or the `apply_patch` tool for file edits so Codex can approve the change, enforce sandbox policy, show diffs, and record rollout."
: " Use the `apply_patch` tool for file edits so Codex can approve the change, enforce sandbox policy, show diffs, and record rollout.";
Expand All @@ -49,11 +53,15 @@ 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.";

export function rejectReadExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
function nativeFilesystemDisabledMessage(codeMode?: boolean): string {
return codeModeBridgeGuidance(codeMode) ?? NATIVE_LOCAL_EXEC_DISABLED;
}

export function rejectReadExecForPolicy(execMsg: ExecServerMessage, codeMode?: boolean): Uint8Array {
if (execMsg.message.case !== "readArgs") throw new Error("invalid read exec");
const path = resolve(execMsg.message.value.path);
return execBytes(execMsg, "readResult", create(ReadResultSchema, {
result: { case: "error", value: create(ReadErrorSchema, { path, error: NATIVE_LOCAL_EXEC_DISABLED }) },
result: { case: "error", value: create(ReadErrorSchema, { path, error: nativeFilesystemDisabledMessage(codeMode) }) },
}));
}

Expand Down Expand Up @@ -87,24 +95,24 @@ export function readExec(execMsg: ExecServerMessage): Uint8Array {
}
}

export function rejectWriteExecForApplyPatch(execMsg: ExecServerMessage, structuredEditAvailable = false): Uint8Array {
export function rejectWriteExecForApplyPatch(execMsg: ExecServerMessage, structuredEditAvailable = false, codeMode?: boolean): Uint8Array {
if (execMsg.message.case !== "writeArgs") throw new Error("invalid write exec");
const path = resolve(execMsg.message.value.path);
return execBytes(execMsg, "writeResult", create(WriteResultSchema, {
result: {
case: "rejected",
value: create(WriteRejectedSchema, { path, reason: codexNativeMutationRefusal("write", structuredEditAvailable) }),
value: create(WriteRejectedSchema, { path, reason: codexNativeMutationRefusal("write", structuredEditAvailable, codeMode) }),
},
}));
}

export function rejectWriteExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
export function rejectWriteExecForPolicy(execMsg: ExecServerMessage, codeMode?: boolean): Uint8Array {
if (execMsg.message.case !== "writeArgs") throw new Error("invalid write exec");
const path = resolve(execMsg.message.value.path);
return execBytes(execMsg, "writeResult", create(WriteResultSchema, {
result: {
case: "rejected",
value: create(WriteRejectedSchema, { path, reason: `${NATIVE_LOCAL_EXEC_DISABLED} No file was changed.` }),
value: create(WriteRejectedSchema, { path, reason: `${nativeFilesystemDisabledMessage(codeMode)} No file was changed.` }),
},
}));
}
Expand Down Expand Up @@ -136,24 +144,24 @@ export function writeExec(execMsg: ExecServerMessage): Uint8Array {
}
}

export function rejectDeleteExecForApplyPatch(execMsg: ExecServerMessage, structuredEditAvailable = false): Uint8Array {
export function rejectDeleteExecForApplyPatch(execMsg: ExecServerMessage, structuredEditAvailable = false, codeMode?: boolean): Uint8Array {
if (execMsg.message.case !== "deleteArgs") throw new Error("invalid delete exec");
const path = resolve(execMsg.message.value.path);
return execBytes(execMsg, "deleteResult", create(DeleteResultSchema, {
result: {
case: "rejected",
value: create(DeleteRejectedSchema, { path, reason: codexNativeMutationRefusal("delete", structuredEditAvailable) }),
value: create(DeleteRejectedSchema, { path, reason: codexNativeMutationRefusal("delete", structuredEditAvailable, codeMode) }),
},
}));
}

export function rejectDeleteExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
export function rejectDeleteExecForPolicy(execMsg: ExecServerMessage, codeMode?: boolean): Uint8Array {
if (execMsg.message.case !== "deleteArgs") throw new Error("invalid delete exec");
const path = resolve(execMsg.message.value.path);
return execBytes(execMsg, "deleteResult", create(DeleteResultSchema, {
result: {
case: "rejected",
value: create(DeleteRejectedSchema, { path, reason: `${NATIVE_LOCAL_EXEC_DISABLED} No file was changed.` }),
value: create(DeleteRejectedSchema, { path, reason: `${nativeFilesystemDisabledMessage(codeMode)} No file was changed.` }),
},
}));
}
Expand Down Expand Up @@ -188,11 +196,11 @@ export function deleteExec(execMsg: ExecServerMessage): Uint8Array {
}
}

export function rejectLsExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
export function rejectLsExecForPolicy(execMsg: ExecServerMessage, codeMode?: boolean): Uint8Array {
if (execMsg.message.case !== "lsArgs") throw new Error("invalid ls exec");
const path = resolve(execMsg.message.value.path);
return execBytes(execMsg, "lsResult", create(LsResultSchema, {
result: { case: "error", value: create(LsErrorSchema, { path, error: NATIVE_LOCAL_EXEC_DISABLED }) },
result: { case: "error", value: create(LsErrorSchema, { path, error: nativeFilesystemDisabledMessage(codeMode) }) },
}));
}

Expand Down Expand Up @@ -256,8 +264,8 @@ function grepError(execMsg: ExecServerMessage, error: string): Uint8Array {
}));
}

export function rejectGrepExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
return grepError(execMsg, NATIVE_LOCAL_EXEC_DISABLED);
export function rejectGrepExecForPolicy(execMsg: ExecServerMessage, codeMode?: boolean): Uint8Array {
return grepError(execMsg, nativeFilesystemDisabledMessage(codeMode));
}

export function grepExec(execMsg: ExecServerMessage): Uint8Array {
Expand Down
12 changes: 12 additions & 0 deletions src/adapters/cursor/native-exec-guidance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const CODE_MODE_BRIDGE_GUIDANCE =
"This request uses Codex code mode. Call the top-level `exec` tool with JavaScript in its body, then call `await tools.exec_command({ ... })` inside that body and emit the result with `text(...)`. Do not call `shell_command`, `exec_command`, or `mcp_opencodex-responses_*` as top-level tools; those names are nested helpers in code mode.";

export function codeModeBridgeGuidance(enabled: boolean | undefined): string | undefined {
return enabled === true ? CODE_MODE_BRIDGE_GUIDANCE : undefined;
}

export function codeModeNestedHelperGuidance(enabled: boolean | undefined): string | undefined {
return enabled === true
? "This request uses Codex code mode. Call the top-level `exec` tool with JavaScript in its body, then call the matching `await tools.<helper>(...)` listed in that tool's description and emit the result with `text(...)`. Do not call nested helper names as top-level tools."
: undefined;
}
5 changes: 3 additions & 2 deletions src/adapters/cursor/native-exec-network.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { create } from "@bufbuild/protobuf";
import { FetchErrorSchema, FetchResultSchema, FetchSuccessSchema, type ExecServerMessage } from "./gen/agent_pb";
import { errorText, execBytes } from "./native-exec-common";
import { codeModeBridgeGuidance } from "./native-exec-guidance";

export interface CursorNativeNetworkDeps {
fetch?: typeof fetch;
Expand All @@ -9,11 +10,11 @@ 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.";

export function rejectFetchExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
export function rejectFetchExecForPolicy(execMsg: ExecServerMessage, codeMode?: boolean): Uint8Array {
if (execMsg.message.case !== "fetchArgs") throw new Error("invalid fetch exec");
const args = execMsg.message.value;
return execBytes(execMsg, "fetchResult", create(FetchResultSchema, {
result: { case: "error", value: create(FetchErrorSchema, { url: args.url, error: NATIVE_FETCH_DISABLED }) },
result: { case: "error", value: create(FetchErrorSchema, { url: args.url, error: codeModeBridgeGuidance(codeMode) ?? NATIVE_FETCH_DISABLED }) },
}));
}

Expand Down
27 changes: 15 additions & 12 deletions src/adapters/cursor/native-exec-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
type ExecServerMessage,
} from "./gen/agent_pb";
import { errorText, execBytes, execStreamCloseBytes } from "./native-exec-common";
import { codeModeBridgeGuidance } from "./native-exec-guidance";
import {
createAdmissionGate,
type AdmissionLease,
Expand Down Expand Up @@ -82,7 +83,9 @@ let unresolvedKills = 0;
let killFailures = 0;

/** Rejection text when Cursor-native shell is denied by policy (issue #604). */
export function nativeShellDisabledMessage(): string {
export function nativeShellDisabledMessage(codeMode?: boolean): string {
const codeModeGuidance = codeModeBridgeGuidance(codeMode);
if (codeModeGuidance) return codeModeGuidance;
// Do not insist on "the same command" — that steers models into replaying bash/CMD
// idioms through the Codex bridge on Windows PowerShell 5.1 and looping (#604).
// Keep this host-shell-neutral: OpenCodex may run on a different OS than the Codex
Expand All @@ -96,7 +99,7 @@ export function nativeShellDisabledMessage(): string {
);
}

function rejectedShellResult(command: string, cwd: string, started: number) {
function rejectedShellResult(command: string, cwd: string, started: number, codeMode?: boolean) {
return create(ShellResultSchema, {
result: {
case: "failure",
Expand All @@ -106,18 +109,18 @@ function rejectedShellResult(command: string, cwd: string, started: number) {
exitCode: 1,
signal: "",
stdout: "",
stderr: nativeShellDisabledMessage(),
stderr: nativeShellDisabledMessage(codeMode),
executionTime: Date.now() - started,
aborted: true,
}),
},
});
}

export function rejectShellExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
export function rejectShellExecForPolicy(execMsg: ExecServerMessage, codeMode?: boolean): Uint8Array {
if (execMsg.message.case !== "shellArgs") throw new Error("invalid shell exec");
const args = execMsg.message.value;
return execBytes(execMsg, "shellResult", rejectedShellResult(args.command, resolve(args.workingDirectory || process.cwd()), Date.now()));
return execBytes(execMsg, "shellResult", rejectedShellResult(args.command, resolve(args.workingDirectory || process.cwd()), Date.now(), codeMode));
}

export function shellExec(execMsg: ExecServerMessage): Uint8Array {
Expand Down Expand Up @@ -155,7 +158,7 @@ export function shellExec(execMsg: ExecServerMessage): Uint8Array {
}));
}

export function rejectShellStreamExecForPolicy(execMsg: ExecServerMessage): Uint8Array[] {
export function rejectShellStreamExecForPolicy(execMsg: ExecServerMessage, codeMode?: boolean): Uint8Array[] {
if (execMsg.message.case !== "shellStreamArgs") throw new Error("invalid shell stream exec");
const args = execMsg.message.value;
const cwd = resolve(args.workingDirectory || process.cwd());
Expand All @@ -165,12 +168,12 @@ export function rejectShellStreamExecForPolicy(execMsg: ExecServerMessage): Uint
event: { case: "start", value: create(ShellStreamStartSchema, { sandboxPolicy: args.requestedSandboxPolicy }) },
})),
execBytes(execMsg, "shellStream", create(ShellStreamSchema, {
event: { case: "stderr", value: create(ShellStreamStderrSchema, { data: nativeShellDisabledMessage() }) },
event: { case: "stderr", value: create(ShellStreamStderrSchema, { data: nativeShellDisabledMessage(codeMode) }) },
})),
execBytes(execMsg, "shellStream", create(ShellStreamSchema, {
event: { case: "exit", value: create(ShellStreamExitSchema, { code: 1, cwd, aborted: true }) },
})),
execBytes(execMsg, "shellResult", rejectedShellResult(args.command, cwd, started)),
execBytes(execMsg, "shellResult", rejectedShellResult(args.command, cwd, started, codeMode)),
execStreamCloseBytes(execMsg),
];
}
Expand Down Expand Up @@ -261,12 +264,12 @@ export async function shellStreamExec(execMsg: ExecServerMessage): Promise<Uint8
return replies;
}

export function rejectBackgroundShellSpawnExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
export function rejectBackgroundShellSpawnExecForPolicy(execMsg: ExecServerMessage, codeMode?: boolean): Uint8Array {
if (execMsg.message.case !== "backgroundShellSpawnArgs") throw new Error("invalid background shell exec");
const args = execMsg.message.value;
const cwd = resolve(args.workingDirectory || process.cwd());
return execBytes(execMsg, "backgroundShellSpawnResult", create(BackgroundShellSpawnResultSchema, {
result: { case: "error", value: create(BackgroundShellSpawnErrorSchema, { command: args.command, workingDirectory: cwd, error: nativeShellDisabledMessage() }) },
result: { case: "error", value: create(BackgroundShellSpawnErrorSchema, { command: args.command, workingDirectory: cwd, error: nativeShellDisabledMessage(codeMode) }) },
}));
}

Expand Down Expand Up @@ -518,10 +521,10 @@ export function backgroundShellSpawnExec(execMsg: ExecServerMessage, sessionId:
}
}

export function rejectWriteShellStdinExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
export function rejectWriteShellStdinExecForPolicy(execMsg: ExecServerMessage, codeMode?: boolean): Uint8Array {
if (execMsg.message.case !== "writeShellStdinArgs") throw new Error("invalid shell stdin exec");
return execBytes(execMsg, "writeShellStdinResult", create(WriteShellStdinResultSchema, {
result: { case: "error", value: create(WriteShellStdinErrorSchema, { error: nativeShellDisabledMessage() }) },
result: { case: "error", value: create(WriteShellStdinErrorSchema, { error: nativeShellDisabledMessage(codeMode) }) },
}));
}

Expand Down
11 changes: 9 additions & 2 deletions src/adapters/cursor/native-exec-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,20 @@ import {
} from "./gen/agent_pb";
import { errorText, execBytes } from "./native-exec-common";
import { OCX_RESPONSES_TOOL_PROVIDER } from "./tool-definitions";
import { codeModeNestedHelperGuidance } from "./native-exec-guidance";

export interface CursorNativeToolDeps {
mcp?: (args: McpArgs) => McpResult | Promise<McpResult>;
listMcpResources?: () => ListMcpResourcesExecResult | Promise<ListMcpResourcesExecResult>;
readMcpResource?: (args: ReadMcpResourceExecArgs) => ReadMcpResourceExecResult | Promise<ReadMcpResourceExecResult>;
computerUse?: (args: ComputerUseArgs) => ComputerUseResult | Promise<ComputerUseResult>;
recordScreen?: (args: RecordScreenArgs) => RecordScreenResult | Promise<RecordScreenResult>;
codeMode?: boolean;
}

function missingMcpResourceExecutorMessage(codeMode?: boolean): string {
const guidance = codeModeNestedHelperGuidance(codeMode);
return guidance ?? "No local MCP resource executor is configured inside opencodex.";
}

export async function mcpExec(execMsg: ExecServerMessage, deps: CursorNativeToolDeps): Promise<Uint8Array> {
Expand Down Expand Up @@ -60,7 +67,7 @@ export async function listMcpResourcesExec(execMsg: ExecServerMessage, deps: Cur
? await deps.listMcpResources()
: create(ListMcpResourcesExecResultSchema, {
result: { case: "error", value: create(ListMcpResourcesErrorSchema, {
error: "No local MCP resource executor is configured inside opencodex.",
error: missingMcpResourceExecutorMessage(deps.codeMode),
}) },
});
return execBytes(execMsg, "listMcpResourcesExecResult", result);
Expand All @@ -73,7 +80,7 @@ export async function readMcpResourceExec(execMsg: ExecServerMessage, deps: Curs
const result = deps.readMcpResource
? await deps.readMcpResource(args)
: create(ReadMcpResourceExecResultSchema, {
result: { case: "error", value: create(ReadMcpResourceErrorSchema, { uri: args.uri, error: "No local MCP resource executor is configured inside opencodex." }) },
result: { case: "error", value: create(ReadMcpResourceErrorSchema, { uri: args.uri, error: missingMcpResourceExecutorMessage(deps.codeMode) }) },
});
return execBytes(execMsg, "readMcpResourceExecResult", result);
} catch (err) {
Expand Down
Loading
Loading