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
41 changes: 41 additions & 0 deletions src/adapters/cursor/tool-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -397,6 +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) {
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 ?? {});
}

Expand All @@ -406,6 +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) {
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);
}
Expand Down
94 changes: 94 additions & 0 deletions tests/providers/cursor/cursor-tool-definitions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -128,6 +130,72 @@ 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("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
Expand All @@ -154,6 +222,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", () => {
Expand Down Expand Up @@ -182,6 +263,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", () => {
Expand Down
Loading