From 1b29236c5bee9dd166b9d23983a2f1f1c2f0b793 Mon Sep 17 00:00:00 2001 From: SB Yoon <44089734+yansigit@users.noreply.github.com> Date: Fri, 4 Sep 2026 20:13:04 -0600 Subject: [PATCH 1/2] fix(cursor): preserve executable tool schemas --- src/adapters/cursor/tool-definitions.ts | 31 ++++++++++ .../cursor/cursor-tool-definitions.test.ts | 60 +++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/src/adapters/cursor/tool-definitions.ts b/src/adapters/cursor/tool-definitions.ts index 31f583572a..ef2abc3faa 100644 --- a/src/adapters/cursor/tool-definitions.ts +++ b/src/adapters/cursor/tool-definitions.ts @@ -50,11 +50,36 @@ export const CURSOR_EXEC_COMMAND_INPUT_SCHEMA = { tty: { type: "boolean", description: "True allocates a PTY for the command; false or omitted uses plain pipes." }, yield_time_ms: { type: "number", description: "Wait before yielding output. Defaults to 10000 ms; effective range is 250-30000 ms." }, max_output_tokens: { type: "number", description: "Output token budget. Defaults to 10000 tokens; larger requests may be capped by policy." }, + sandbox_permissions: { + type: "string", + enum: ["use_default", "require_escalated"], + description: "Per-command sandbox override. Defaults to use_default; use require_escalated for unsandboxed execution.", + }, + justification: { + type: "string", + description: "User-facing approval question for require_escalated; omit otherwise.", + }, + prefix_rule: { + type: "array", + items: { type: "string" }, + description: "Reusable approval prefix for cmd, only with sandbox_permissions: require_escalated.", + }, + login: { + type: "boolean", + description: "True runs the shell with login semantics; false disables them. Defaults to true.", + }, }, required: ["cmd"], additionalProperties: false, } as const; +/** Cursor represents a Responses freeform tool body as one string-valued input field. */ +export const CURSOR_FREEFORM_INPUT_SCHEMA = { + type: "object", + properties: { input: { type: "string" } }, + required: ["input"], +} as const; + /** * Structured single-replacement schema advertised to Cursor models in addition to the freeform * `apply_patch` tool. Cursor-trained models reliably emit exact-match replacements (the native @@ -111,6 +136,10 @@ export const CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA = { yield_time_ms: { type: "number", description: "Wait before yielding output. Defaults to 10000 ms; effective range is 250-30000 ms." }, max_output_tokens: { type: "number", description: "Output token budget. Defaults to 10000 tokens; larger requests may be capped by policy." }, max_output_chars: { type: "number", description: "Output character budget when the Responses tool uses chars instead of tokens." }, + sandbox_permissions: { type: "string", enum: ["use_default", "require_escalated"] }, + justification: { type: "string" }, + prefix_rule: { type: "array", items: { type: "string" } }, + login: { type: "boolean" }, }, required: ["command"], } as const; @@ -397,6 +426,7 @@ export function responsesToolNameFromCursorWire(name: string, cursorToolNameMap? /** Schema advertised to Cursor for this tool (may use Cursor-preferred field names like `cmd`). */ export function cursorToolInputSchema(tool: OcxTool): unknown { + if (tool.freeform) return CURSOR_FREEFORM_INPUT_SCHEMA; return isBareCodexExecCommandTool(tool) ? CURSOR_EXEC_COMMAND_INPUT_SCHEMA : (tool.parameters ?? {}); } @@ -406,6 +436,7 @@ export function cursorToolInputSchema(tool: OcxTool): unknown { * treating `cmd` as canonical prevents the `cmd` → `command` rewrite Codex requires (#399). */ export function cursorToolArgNormalizeSchema(tool: OcxTool): unknown { + if (tool.freeform) return CURSOR_FREEFORM_INPUT_SCHEMA; if (isBareCodexShellBridgeTool(tool)) { return shellBridgeArgNormalizeSchema(tool); } diff --git a/tests/providers/cursor/cursor-tool-definitions.test.ts b/tests/providers/cursor/cursor-tool-definitions.test.ts index f21ae2fb27..09166be47f 100644 --- a/tests/providers/cursor/cursor-tool-definitions.test.ts +++ b/tests/providers/cursor/cursor-tool-definitions.test.ts @@ -7,7 +7,9 @@ import { buildCursorToolDefinitions, cursorToolsForActivePrompt, buildCursorToolGuidanceSystemNote, + CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA, CURSOR_EXEC_COMMAND_INPUT_SCHEMA, + CURSOR_FREEFORM_INPUT_SCHEMA, cursorRequestAdvertisesApplyPatch, cursorRequestUsesCodeMode, isCursorCodeModeExecTool, @@ -128,6 +130,38 @@ describe("Cursor tool definitions", () => { expect(toJson(ValueSchema, fromBinary(ValueSchema, defs[0]!.inputSchema))).toEqual(CURSOR_EXEC_COMMAND_INPUT_SCHEMA); }); + test("preserves sandbox escalation controls in shell advertisement and normalization", () => { + const advertised = CURSOR_EXEC_COMMAND_INPUT_SCHEMA.properties; + const normalized = CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA.properties; + + expect(advertised.sandbox_permissions.enum).toEqual(["use_default", "require_escalated"]); + expect(advertised.justification.type).toBe("string"); + expect(advertised.prefix_rule.items).toEqual({ type: "string" }); + expect(advertised.login.type).toBe("boolean"); + expect(normalized.sandbox_permissions.enum).toEqual(["use_default", "require_escalated"]); + expect(normalized.justification.type).toBe("string"); + expect(normalized.prefix_rule.items).toEqual({ type: "string" }); + expect(normalized.login.type).toBe("boolean"); + }); + + test("advertises and normalizes freeform tools as one required string input", () => { + const tool: OcxTool = { + name: "apply_patch", + description: "Apply a patch", + parameters: {}, + freeform: true, + }; + + expect(cursorToolInputSchema(tool)).toEqual(CURSOR_FREEFORM_INPUT_SCHEMA); + expect(cursorToolArgNormalizeSchema(tool)).toEqual(CURSOR_FREEFORM_INPUT_SCHEMA); + const defs = buildCursorToolDefinitions([tool]); + expect(toJson(ValueSchema, fromBinary(ValueSchema, defs[0]!.inputSchema))).toEqual(CURSOR_FREEFORM_INPUT_SCHEMA); + + const codeModeExec: OcxTool = { name: "exec", description: "Run JavaScript", freeform: true }; + expect(cursorToolInputSchema(codeModeExec)).toEqual(CURSOR_FREEFORM_INPUT_SCHEMA); + expect(cursorToolArgNormalizeSchema(codeModeExec)).toEqual(CURSOR_FREEFORM_INPUT_SCHEMA); + }); + test("normalizes advertised shell_command cmd args to Responses command before Codex sees them", () => { // Live #399 failure: Cursor advertisement requires `cmd`, models send `cmd`, but Codex // shell_command validates `command` → "missing field `command`". Normalization must use the @@ -154,6 +188,19 @@ describe("Cursor tool definitions", () => { expect(normalizeArgKeys({ command: "git status" }, cursorToolArgNormalizeSchema(tool))).toEqual({ command: "git status", }); + expect(normalizeArgKeys({ + cmd: "git status", + sandbox_permissions: "require_escalated", + justification: "Fetch the requested upstream ref", + prefix_rule: ["git", "fetch"], + login: false, + }, cursorToolArgNormalizeSchema(tool))).toEqual({ + command: "git status", + sandbox_permissions: "require_escalated", + justification: "Fetch the requested upstream ref", + prefix_rule: ["git", "fetch"], + login: false, + }); }); test("preserves cmd-only exec_command schemas during Responses normalization", () => { @@ -182,6 +229,19 @@ describe("Cursor tool definitions", () => { cmd: "git status", workdir: "C:/repo", }); + expect(normalizeArgKeys({ + cmd: "git fetch", + sandbox_permissions: "require_escalated", + justification: "Fetch the requested upstream ref", + prefix_rule: ["git", "fetch"], + login: false, + }, cursorToolArgNormalizeSchema(tool))).toEqual({ + cmd: "git fetch", + sandbox_permissions: "require_escalated", + justification: "Fetch the requested upstream ref", + prefix_rule: ["git", "fetch"], + login: false, + }); }); test("shell bridge command validation honors the schema-required command key", () => { From 37e6115c8a2ad3ffe20fee1e5a1e79a054625a56 Mon Sep 17 00:00:00 2001 From: SB Yoon <44089734+yansigit@users.noreply.github.com> Date: Fri, 4 Sep 2026 23:44:57 -0600 Subject: [PATCH 2/2] fix(cursor): reject reserved freeform shell names --- src/adapters/cursor/tool-definitions.ts | 14 ++++++-- .../cursor/cursor-tool-definitions.test.ts | 34 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/adapters/cursor/tool-definitions.ts b/src/adapters/cursor/tool-definitions.ts index ef2abc3faa..519e6f0971 100644 --- a/src/adapters/cursor/tool-definitions.ts +++ b/src/adapters/cursor/tool-definitions.ts @@ -426,7 +426,12 @@ export function responsesToolNameFromCursorWire(name: string, cursorToolNameMap? /** Schema advertised to Cursor for this tool (may use Cursor-preferred field names like `cmd`). */ export function cursorToolInputSchema(tool: OcxTool): unknown { - if (tool.freeform) return CURSOR_FREEFORM_INPUT_SCHEMA; + if (tool.freeform) { + if (isBareCodexShellBridgeTool(tool)) { + throw new Error(`freeform Cursor tools cannot use reserved shell bridge name ${tool.name}; use a namespace`); + } + return CURSOR_FREEFORM_INPUT_SCHEMA; + } return isBareCodexExecCommandTool(tool) ? CURSOR_EXEC_COMMAND_INPUT_SCHEMA : (tool.parameters ?? {}); } @@ -436,7 +441,12 @@ export function cursorToolInputSchema(tool: OcxTool): unknown { * treating `cmd` as canonical prevents the `cmd` → `command` rewrite Codex requires (#399). */ export function cursorToolArgNormalizeSchema(tool: OcxTool): unknown { - if (tool.freeform) return CURSOR_FREEFORM_INPUT_SCHEMA; + if (tool.freeform) { + if (isBareCodexShellBridgeTool(tool)) { + throw new Error(`freeform Cursor tools cannot use reserved shell bridge name ${tool.name}; use a namespace`); + } + return CURSOR_FREEFORM_INPUT_SCHEMA; + } if (isBareCodexShellBridgeTool(tool)) { return shellBridgeArgNormalizeSchema(tool); } diff --git a/tests/providers/cursor/cursor-tool-definitions.test.ts b/tests/providers/cursor/cursor-tool-definitions.test.ts index 09166be47f..c86e9bf30d 100644 --- a/tests/providers/cursor/cursor-tool-definitions.test.ts +++ b/tests/providers/cursor/cursor-tool-definitions.test.ts @@ -162,6 +162,40 @@ describe("Cursor tool definitions", () => { expect(cursorToolArgNormalizeSchema(codeModeExec)).toEqual(CURSOR_FREEFORM_INPUT_SCHEMA); }); + test("rejects freeform tools that reuse bare shell bridge names", () => { + for (const name of ["exec_command", "shell_command"]) { + const tool: OcxTool = { name, description: "Custom", parameters: {}, freeform: true }; + + expect(() => cursorToolInputSchema(tool)).toThrow(`freeform Cursor tools cannot use reserved shell bridge name ${name}`); + expect(() => cursorToolArgNormalizeSchema(tool)).toThrow(`freeform Cursor tools cannot use reserved shell bridge name ${name}`); + expect(() => buildCursorToolDefinitions([tool])).toThrow(`freeform Cursor tools cannot use reserved shell bridge name ${name}`); + } + }); + + test("preserves namespaced shell names and ordinary freeform/non-freeform contracts", () => { + const namespacedFreeform: OcxTool = { + name: "exec_command", + namespace: "mcp__custom", + description: "Custom", + parameters: {}, + freeform: true, + }; + expect(cursorToolInputSchema(namespacedFreeform)).toEqual(CURSOR_FREEFORM_INPUT_SCHEMA); + expect(cursorToolArgNormalizeSchema(namespacedFreeform)).toEqual(CURSOR_FREEFORM_INPUT_SCHEMA); + + const ordinaryFreeform: OcxTool = { name: "apply_patch", description: "Patch", parameters: {}, freeform: true }; + expect(cursorToolInputSchema(ordinaryFreeform)).toEqual(CURSOR_FREEFORM_INPUT_SCHEMA); + expect(cursorToolArgNormalizeSchema(ordinaryFreeform)).toEqual(CURSOR_FREEFORM_INPUT_SCHEMA); + + const ordinaryFunction: OcxTool = { + name: "exec_command", + description: "Run", + parameters: { type: "object", properties: { cmd: { type: "string" } }, required: ["cmd"] }, + }; + expect(cursorToolInputSchema(ordinaryFunction)).toEqual(CURSOR_EXEC_COMMAND_INPUT_SCHEMA); + expect(cursorToolArgNormalizeSchema(ordinaryFunction)).toEqual(ordinaryFunction.parameters); + }); + test("normalizes advertised shell_command cmd args to Responses command before Codex sees them", () => { // Live #399 failure: Cursor advertisement requires `cmd`, models send `cmd`, but Codex // shell_command validates `command` → "missing field `command`". Normalization must use the