Skip to content

Commit f708d2b

Browse files
authored
Merge pull request #675 from Wibias/sync/627-to-dev2-go
fix(cursor): carry Windows bridge shell guidance onto dev2-go (#627)
2 parents 2d1d4bf + d44e567 commit f708d2b

4 files changed

Lines changed: 48 additions & 7 deletions

File tree

src/adapters/cursor/native-exec-shell.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,20 @@ import { errorText, execBytes, execStreamCloseBytes } from "./native-exec-common
2323
const backgroundShells = new Map<number, { child: ChildProcessWithoutNullStreams; outputLength: number }>();
2424
let nextShellId = 1;
2525

26-
const NATIVE_SHELL_DISABLED =
27-
"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 with the same command.";
26+
/** Rejection text when Cursor-native shell is denied by policy (issue #604). */
27+
export function nativeShellDisabledMessage(): string {
28+
// Do not insist on "the same command" — that steers models into replaying bash/CMD
29+
// idioms through the Codex bridge on Windows PowerShell 5.1 and looping (#604).
30+
// Keep this host-shell-neutral: OpenCodex may run on a different OS than the Codex
31+
// client that executes the bridge (LAN/SSH remote-proxy).
32+
return (
33+
"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). "
34+
+ "Do not tell the user shell access is blocked, disabled, or denied — silently call that bridge tool. "
35+
+ "Adapt the command for the Codex client host shell before calling the bridge "
36+
+ "(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 `&&`). "
37+
+ "Make at most one corrected bridge attempt after a failure, then report the error and stop — do not repeat equivalent failing commands."
38+
);
39+
}
2840

2941
function rejectedShellResult(command: string, cwd: string, started: number) {
3042
return create(ShellResultSchema, {
@@ -36,7 +48,7 @@ function rejectedShellResult(command: string, cwd: string, started: number) {
3648
exitCode: 1,
3749
signal: "",
3850
stdout: "",
39-
stderr: NATIVE_SHELL_DISABLED,
51+
stderr: nativeShellDisabledMessage(),
4052
executionTime: Date.now() - started,
4153
aborted: true,
4254
}),
@@ -95,7 +107,7 @@ export function rejectShellStreamExecForPolicy(execMsg: ExecServerMessage): Uint
95107
event: { case: "start", value: create(ShellStreamStartSchema, { sandboxPolicy: args.requestedSandboxPolicy }) },
96108
})),
97109
execBytes(execMsg, "shellStream", create(ShellStreamSchema, {
98-
event: { case: "stderr", value: create(ShellStreamStderrSchema, { data: NATIVE_SHELL_DISABLED }) },
110+
event: { case: "stderr", value: create(ShellStreamStderrSchema, { data: nativeShellDisabledMessage() }) },
99111
})),
100112
execBytes(execMsg, "shellStream", create(ShellStreamSchema, {
101113
event: { case: "exit", value: create(ShellStreamExitSchema, { code: 1, cwd, aborted: true }) },
@@ -196,7 +208,7 @@ export function rejectBackgroundShellSpawnExecForPolicy(execMsg: ExecServerMessa
196208
const args = execMsg.message.value;
197209
const cwd = resolve(args.workingDirectory || process.cwd());
198210
return execBytes(execMsg, "backgroundShellSpawnResult", create(BackgroundShellSpawnResultSchema, {
199-
result: { case: "error", value: create(BackgroundShellSpawnErrorSchema, { command: args.command, workingDirectory: cwd, error: NATIVE_SHELL_DISABLED }) },
211+
result: { case: "error", value: create(BackgroundShellSpawnErrorSchema, { command: args.command, workingDirectory: cwd, error: nativeShellDisabledMessage() }) },
200212
}));
201213
}
202214

@@ -233,7 +245,7 @@ export function backgroundShellSpawnExec(execMsg: ExecServerMessage): Uint8Array
233245
export function rejectWriteShellStdinExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
234246
if (execMsg.message.case !== "writeShellStdinArgs") throw new Error("invalid shell stdin exec");
235247
return execBytes(execMsg, "writeShellStdinResult", create(WriteShellStdinResultSchema, {
236-
result: { case: "error", value: create(WriteShellStdinErrorSchema, { error: NATIVE_SHELL_DISABLED }) },
248+
result: { case: "error", value: create(WriteShellStdinErrorSchema, { error: nativeShellDisabledMessage() }) },
237249
}));
238250
}
239251

src/adapters/cursor/tool-definitions.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,11 @@ export function buildCursorToolGuidanceSystemNote(
417417
const hasApplyPatch = cursorRequestAdvertisesApplyPatch(tools, toolChoice);
418418
const discoveryTools = discoveryToolLabel(wireNames);
419419
const unavailableNeighborNames = unavailableNeighborAgentToolNames(wireNames);
420+
// Host-shell-neutral: the Codex client executes bridge commands, and may differ from
421+
// the OpenCodex proxy OS (LAN/SSH remote-proxy). Always cover PowerShell 5.1 pitfalls.
422+
const hostShellNote = hasBareExec
423+
? "Match shell syntax to the Codex client host that runs the bridge (not only the proxy OS). Windows PowerShell 5.1: no CMD `cd /d`, no bash heredocs (`<<EOF`); `&&`/`||` 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 `&&`. POSIX: use portable commands. After a shell failure, make at most one corrected bridge attempt, then report the error and stop — do not repeat equivalent failing commands."
424+
: undefined;
420425
const notes = [
421426
`Cursor tool calls: available tool names are exactly ${listedNames}.`,
422427
"Use the current tool catalog as ground truth and call only those exact names with their listed argument keys.",
@@ -432,6 +437,7 @@ export function buildCursorToolGuidanceSystemNote(
432437
hasBareExec
433438
? "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`."
434439
: undefined,
440+
hostShellNote,
435441
"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.",
436442
hasBareExec
437443
? `For file read/search/listing, use ${shellBridgeLabel} when no more specific listed tool is available.`
@@ -451,7 +457,7 @@ export function buildCursorToolGuidanceSystemNote(
451457
: undefined,
452458
"Do not count or report a tool call unless a tool result was actually returned.",
453459
hasBareExec
454-
? `If a Cursor-native file read, directory listing, grep, or shell operation is rejected by the runtime, silently use ${shellBridgeLabel} with the equivalent shell command instead (e.g. \`cat\`, \`ls\`, \`rg\`, \`grep\`). Do not tell the user access is blocked. For file edits, use \`apply_patch\` when available.`
460+
? `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 \`apply_patch\` when available.`
455461
: undefined,
456462
].filter((note): note is string => typeof note === "string");
457463
return notes.join(" ");

tests/cursor-native-exec-policy.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,12 @@ describe("Cursor native exec sandbox policy", () => {
142142
expect(deniedShellText).toContain("exec_command");
143143
expect(deniedShellText).toContain("mcp_opencodex-responses_*");
144144
expect(deniedShellText).toContain("Do not tell the user");
145+
expect(deniedShellText).not.toContain("with the same command");
146+
expect(deniedShellText).toContain("at most one corrected bridge attempt");
147+
expect(deniedShellText).toContain("if ($?)");
148+
expect(deniedShellText).toContain("`&&`/`||` are unsupported parser errors");
149+
expect(deniedShellText).toContain("do not treat `;` as a substitute for `&&`");
150+
expect(deniedShellText).toContain("Windows PowerShell 5.1");
145151
expect(deniedShellText).not.toContain("disabled by OpenCodex policy");
146152
expect(deniedShellText).not.toContain("sandbox denial");
147153
expect(deniedShell.message.case).toBe("shellResult");

tests/cursor-tool-definitions.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,23 @@ describe("Cursor tool definitions", () => {
324324
expect(note).toContain("Never tell the user that shell or read access is blocked");
325325
});
326326

327+
test("adds host-shell-neutral PowerShell and one-retry-stop guidance (#604)", () => {
328+
const note = buildCursorToolGuidanceSystemNote([{ name: "shell_command", description: "Run", parameters: {} }]);
329+
expect(note).toBeDefined();
330+
if (!note) throw new Error("Expected Cursor tool guidance note");
331+
332+
expect(note).toContain("Windows PowerShell 5.1");
333+
expect(note).toContain("cd /d");
334+
expect(note).toContain("<<EOF");
335+
expect(note).toContain("if ($?)");
336+
expect(note).toContain("`&&`/`||` are unsupported parser errors");
337+
expect(note).toContain("do not treat `;` as a substitute for `&&`");
338+
expect(note).toContain("at most one corrected bridge attempt");
339+
expect(note).toContain("Get-Content");
340+
expect(note).toContain("`cat`/`ls`/`rg`");
341+
expect(note).toContain("Codex client host");
342+
});
343+
327344
test("adds codex-native edit guidance only when apply_patch is advertised", () => {
328345
const tools: OcxTool[] = [
329346
{ name: "exec_command", description: "Run", parameters: {} },

0 commit comments

Comments
 (0)