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
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Cursor schema cycle P refresh

Parent: d6bfb044a5dc6494cba57c1238ded7c23faf5586, open PR #3702. Original #3628 remains at 37e6115c8a2ad3ffe20fee1e5a1e79a054625a56, author SB Yoon (yansigit).

The source commits 1b29236c5bee9dd166b9d23983a2f1f1c2f0b793 and 37e6115c8a2ad3ffe20fee1e5a1e79a054625a56 are prepared as mailbox patches with only production diff paths mapped from tool-definitions.ts to current tool-schemas.ts. `git apply --check` accepted the first mapped patch. Apply both in order during B, retaining their original author/date/message. Main then adds the new constant to the existing public re-export, closes the freeform object with additionalProperties:false, strengthens literal/protobuf assertions and updates the planned docs/structure.

The current naming path preserves namespaces through namespacedToolName; the existing bare-shell helper remains the authority for the original rejection. No tool execution or approval policy changes are introduced. Read current 030 for all activation cases and complete scope.

Main owns authored patch application, public facade and documentation edits, commits and PR publication. An inherited worker may amend only tool-schemas.ts and cursor-tool-definitions.test.ts after A; no Git or local tests/typecheck. Independent review plus current-head remote full/typecheck/docs and hosted CI supply proof. The candidate can remain open in the stack while shipping/closure criteria remain separately pending.
6 changes: 6 additions & 0 deletions docs-site/src/content/docs/reference/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,12 @@ compatibility pair: `agent.v1.AgentService/RunSSE` for server output and
broader built-in executor and bypasses Codex approval/sandbox semantics, and legacy
`unsafeAllowNativeLocalExec: true` remains equivalent only when `nativeLocalExec` is unset.

Codex-compatible shell schemas retain sandbox permissions, justification, reusable
prefix rules and login mode. Freeform tools expose one required string `input`;
bare `exec_command` and `shell_command` names are reserved for non-freeform shell
bridges. Namespace a custom freeform tool that uses either name. These schema
declarations do not grant approval or change execution policy.

## `azure-openai` (alias: `azure`)

**Targets:** **Azure OpenAI**. Wraps `openai-responses` (so also `passthrough: true`).
Expand Down
2 changes: 1 addition & 1 deletion src/adapters/cursor/tool-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { McpToolDefinitionSchema, McpToolsSchema, type McpToolDefinition } from
import { CURSOR_EDIT_FILE_TOOL, CURSOR_MULTI_EDIT_TOOL, cursorRequestAdvertisesApplyPatch, cursorToolAllowedByChoice, cursorToolWireName, OCX_RESPONSES_TOOL_PROVIDER } from "./tool-naming";
import { CURSOR_EDIT_FILE_INPUT_SCHEMA, CURSOR_MULTI_EDIT_INPUT_SCHEMA, cursorToolInputSchema } from "./tool-schemas";
export { OCX_RESPONSES_TOOL_PROVIDER, CODEX_EXEC_COMMAND_TOOL, CODEX_SHELL_COMMAND_TOOL, CODEX_UNIFIED_EXEC_TOOL, CODEX_WAIT_TOOL, CODEX_APPLY_PATCH_TOOL, CODEX_TOOL_SEARCH_TOOL, CURSOR_EDIT_FILE_TOOL, CURSOR_MULTI_EDIT_TOOL, CURSOR_STRUCTURED_EDIT_TOOLS, CURSOR_EXEC_COMMAND_TOOL, CODEX_SHELL_BRIDGE_TOOL_NAMES, isCodexShellBridgeToolName, resolveShellBridgeAliasKey, cursorToolChoiceAliases, isBareCodexShellBridgeTool, isCursorExecutionPathTool, isCursorWaitTool, isCursorCodeModeExecTool, cursorRequestUsesCodeMode, cursorRequestHasShellAlias, cursorRequestAdvertisesApplyPatch, isCursorStructuredEditToolName, isCursorSyntheticStructuredEditTool, cursorToolWireName, normalizeCursorWireName, normalizeCursorTextToolMarkers, responsesToolNameFromCursorWire, cursorToolAllowedByChoice } from "./tool-naming";
export { CURSOR_EXEC_COMMAND_INPUT_SCHEMA, CURSOR_EDIT_FILE_INPUT_SCHEMA, CURSOR_MULTI_EDIT_INPUT_SCHEMA, CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA, cursorToolInputSchema, cursorToolArgNormalizeSchema, shellBridgeRequiredCommandKeys, defaultShellBridgeArgNormalizeSchema, cursorShellBridgeDropError, nonEmptyShellBridgeCommandFromArgs, cursorShellBridgeArgsValid } from "./tool-schemas";
export { CURSOR_EXEC_COMMAND_INPUT_SCHEMA, CURSOR_FREEFORM_INPUT_SCHEMA, CURSOR_EDIT_FILE_INPUT_SCHEMA, CURSOR_MULTI_EDIT_INPUT_SCHEMA, CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA, cursorToolInputSchema, cursorToolArgNormalizeSchema, shellBridgeRequiredCommandKeys, defaultShellBridgeArgNormalizeSchema, cursorShellBridgeDropError, nonEmptyShellBridgeCommandFromArgs, cursorShellBridgeArgsValid } from "./tool-schemas";
export { CURSOR_SHELL_ALIAS_SYSTEM_NOTE, CURSOR_GENERIC_TOOL_USE_USER_HINT, isGenericToolUseCountDemoPrompt, requestedCursorToolUseCount, shouldAppendCursorGenericToolUseHint, appendCursorGenericToolUseHint, shouldUseNativeExecOnlyForGenericToolUse, cursorToolsForActivePrompt, buildCursorToolGuidanceSystemNote } from "./tool-guidance";

/**
Expand Down
42 changes: 42 additions & 0 deletions src/adapters/cursor/tool-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,37 @@ 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"],
additionalProperties: false,
} 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 @@ -71,13 +97,23 @@ 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;


/** 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;
Comment thread
lidge-jun marked this conversation as resolved.
}
return isBareCodexExecCommandTool(tool) ? CURSOR_EXEC_COMMAND_INPUT_SCHEMA : (tool.parameters ?? {});
}

Expand All @@ -87,6 +123,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
11 changes: 11 additions & 0 deletions structure/04_transports-and-sidecars.md
Original file line number Diff line number Diff line change
Expand Up @@ -1528,6 +1528,17 @@ parallel/colliding identities, distinct unsafe raw JSON index literals, the maxi
safe-integer boundary, invalid index types, missing/null continuations and UTF-8
byte-limit boundaries.

## Cursor executable tool schema ownership

`src/adapters/cursor/tool-schemas.ts` owns advertised and argument-normalization
schemas; `tool-definitions.ts` remains the public facade and protobuf encoder.
Advertisement and normalization intentionally differ for shell bridges: Cursor may
emit `cmd`, while the declared Responses contract decides whether it becomes
`command`. Both paths preserve execution-control fields. Freeform tools use one
required string `input`; bare shell bridge names are rejected on the freeform path.
Namespaced tools do not acquire bare-shell behavior. Regression coverage lives in
`tests/providers/cursor/cursor-tool-definitions.test.ts`.

## Sidecars

Web search and vision sidecars run only when the main request needs that capability and a usable
Expand Down
117 changes: 117 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,95 @@ 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", () => {
// Independent wire contract: using the production constant as the expected value
// would let an incorrect constant validate both schema selection and protobuf output.
const expectedSchema = {
type: "object",
properties: { input: { type: "string" } },
required: ["input"],
additionalProperties: false,
};
const tool: OcxTool = {
name: "apply_patch",
description: "Apply a patch",
parameters: {},
freeform: true,
};

expect(CURSOR_FREEFORM_INPUT_SCHEMA).toEqual(expectedSchema);
expect(cursorToolInputSchema(tool)).toEqual(expectedSchema);
expect(cursorToolArgNormalizeSchema(tool)).toEqual(expectedSchema);
const defs = buildCursorToolDefinitions([tool]);
expect(defs).toHaveLength(1);
expect(toJson(ValueSchema, fromBinary(ValueSchema, defs[0]!.inputSchema))).toEqual(expectedSchema);

const codeModeExec: OcxTool = { name: "exec", description: "Run JavaScript", freeform: true };
expect(cursorToolInputSchema(codeModeExec)).toEqual(expectedSchema);
expect(cursorToolArgNormalizeSchema(codeModeExec)).toEqual(expectedSchema);
const execDefs = buildCursorToolDefinitions([codeModeExec]);
expect(execDefs).toHaveLength(1);
expect(toJson(ValueSchema, fromBinary(ValueSchema, execDefs[0]!.inputSchema))).toEqual(expectedSchema);
});

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 expectedFreeformSchema = {
type: "object",
properties: { input: { type: "string" } },
required: ["input"],
additionalProperties: false,
};
const namespacedFreeform: OcxTool = {
name: "exec_command",
namespace: "mcp__custom",
description: "Custom",
parameters: {},
freeform: true,
};
expect(cursorToolInputSchema(namespacedFreeform)).toEqual(expectedFreeformSchema);
expect(cursorToolArgNormalizeSchema(namespacedFreeform)).toEqual(expectedFreeformSchema);
const defs = buildCursorToolDefinitions([namespacedFreeform]);
expect(defs).toHaveLength(1);
expect(defs[0]?.toolName).toBe("mcp__custom__exec_command");
expect(toJson(ValueSchema, fromBinary(ValueSchema, defs[0]!.inputSchema))).toEqual(expectedFreeformSchema);

const ordinaryFreeform: OcxTool = { name: "apply_patch", description: "Patch", parameters: {}, freeform: true };
expect(cursorToolInputSchema(ordinaryFreeform)).toEqual(expectedFreeformSchema);
expect(cursorToolArgNormalizeSchema(ordinaryFreeform)).toEqual(expectedFreeformSchema);

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 +245,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 +286,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