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
8 changes: 7 additions & 1 deletion docs-site/src/content/docs/reference/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,13 @@ compatibility pair: `agent.v1.AgentService/RunSSE` for server output and
Foreground `shellArgs` and `shellStreamArgs` are an exception: both are rejected before spawn
on every platform until kernel-backed descendant ownership is available. Use client shell tools;
background-shell execution and other native operations retain their existing policy.
- The denial reply is a silent redirect whose wording follows the request catalog.
- The denial reply follows the request catalog. Explicit Claude-family Cursor models receive
factual wording that names the available tools without instructions to hide or avoid discussing
the redirect. Factual routing commentary can precede the next tool call without ending the turn.
When no client or configured MCP tools are available, Claude models are asked to answer without
tools or report the limitation instead of calling nonexistent shell/edit tools.
Other models, including Auto/default, retain the existing silent-redirect wording and commentary
guard. Tool availability, execution policy, and approval/sandbox behavior are unchanged.
In code mode — a freeform unified `exec` and no bare shell bridge — the redirect points inside `exec`, where
shell, file, search, and fetch are nested `tools.<name>(...)` helpers of the JavaScript cell,
and never recommends the top-level shell bridge code mode does not expose. A flat catalog that
Expand Down
2 changes: 1 addition & 1 deletion scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -819,7 +819,7 @@
"cursor-tool-finalize-race.test.ts": "providers/cursor",
"cursor-tool-result-image.test.ts": "providers/cursor",
"cursor-tool-result-invocation.test.ts": "providers/cursor",
"cursor-tool-suspended-checkpoint.test.ts": "providers/cursor",
"cursor-tool-suspended-checkpoint.test.ts": "providers/cursor", "cursor-tool-wording.test.ts": "providers/cursor",
"cursor-toolresult-normalize.test.ts": "providers/cursor",
"cursor-transport-retry.test.ts": "providers/cursor",
"cursor-ultra-mode.test.ts": "providers/cursor",
Expand Down
3 changes: 3 additions & 0 deletions src/adapters/cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import {
type CursorTransportFactory,
} from "./cursor/transport";
import { cursorLiveRosterScope } from "./cursor/catalog";
import { cursorUsesPlainToolWording } from "./cursor/tool-wording";

export const CURSOR_API_URL = "https://api2.cursor.sh";

Expand Down Expand Up @@ -313,8 +314,10 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
const armMidstreamEcho = cursorNeedsExternalToolContinuation(activeRequest.modelId) && replaysToolResult;
const midstreamObserver = armMidstreamEcho ? new CursorMidstreamEchoObserver() : undefined;
const midstreamFilter = armMidstreamEcho ? new ToolEnvelopeEchoFilter() : undefined;
// Claude's factual redirects allow truthful routing prose; it is not a failure claim.
const armRoutingCommentarySniffer =
isCursorExternalWireModel(activeRequest.modelId)
&& !cursorUsesPlainToolWording(activeRequest.modelId)
&& (
cursorRequestUsesCodeMode(activeRequest.tools, activeRequest.toolChoice)
|| cursorRequestHasShellAlias(activeRequest.tools)
Expand Down
4 changes: 3 additions & 1 deletion src/adapters/cursor/live-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import { activePromptText, prepareCursorRunRequest } from "./protobuf-request";
import { prepareCursorRawMessages, resolveActiveCursorImages } from "./images";
import { isCursorExternalWireModel } from "./discovery";
import { cursorUsesPlainToolWording } from "./tool-wording";
import { cursorRequestMessagesFromRaw } from "./request-builder";
import {
createCursorContextUsageTracker,
Expand Down Expand Up @@ -718,7 +719,8 @@ class LiveCursorTransport implements CursorTransport {
clientToolDefs,
rejectNativeFileMutations: cursorRequestAdvertisesApplyPatch(request.tools, request.toolChoice),
structuredEditAvailable: syntheticStructuredEditToolNames.size > 0,
nativeExecRedirectHint: cursorNativeExecRedirectHint(cursorVisibleTools, this.execContext.mcpToolDefs ?? []),
nativeExecRedirectHint: cursorNativeExecRedirectHint(cursorVisibleTools, this.execContext.mcpToolDefs ?? [], activeRequest.modelId),
plainToolWording: cursorUsesPlainToolWording(activeRequest.modelId),
};
const toolSchemas = new Map<string, unknown>();
const cursorToolNameMap = new Map<string, string>();
Expand Down
21 changes: 12 additions & 9 deletions src/adapters/cursor/native-exec-fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,17 @@ import {
} from "./gen/agent_pb";
import { errorText, execBytes, lineCount, textDecoder, textEncoder } from "./native-exec-common";

import { CURSOR_TOOL_CALL_CONTINUATION } from "./tool-wording";

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, plainToolWording = false): string {
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.";
if (plainToolWording) return `Cursor-native ${operation} is not available for this request.${structuredHint} No file was changed. ${CURSOR_TOOL_CALL_CONTINUATION}`;
return `Make this ${operation} through the Codex edit path instead.${structuredHint} No file was changed. Do NOT narrate this redirect or comment on tool availability — just make the edit call.`;
}

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

export function rejectWriteExecForApplyPatch(execMsg: ExecServerMessage, structuredEditAvailable = false): Uint8Array {
export function rejectWriteExecForApplyPatch(execMsg: ExecServerMessage, structuredEditAvailable = false, plainToolWording = false): 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, plainToolWording) }),
},
}));
}

export function rejectWriteExecForPolicy(execMsg: ExecServerMessage, hint?: string): Uint8Array {
export function rejectWriteExecForPolicy(execMsg: ExecServerMessage, hint?: string, plainToolWording = false): 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: `${hint ?? NATIVE_LOCAL_EXEC_DISABLED} No file was changed.` }),
value: create(WriteRejectedSchema, { path, reason: plainToolWording ? `No file was changed. ${hint ?? NATIVE_LOCAL_EXEC_DISABLED}` : `${hint ?? NATIVE_LOCAL_EXEC_DISABLED} No file was changed.` }),
},
}));
}
Expand Down Expand Up @@ -136,24 +139,24 @@ export function writeExec(execMsg: ExecServerMessage): Uint8Array {
}
}

export function rejectDeleteExecForApplyPatch(execMsg: ExecServerMessage, structuredEditAvailable = false): Uint8Array {
export function rejectDeleteExecForApplyPatch(execMsg: ExecServerMessage, structuredEditAvailable = false, plainToolWording = false): 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, plainToolWording) }),
},
}));
}

export function rejectDeleteExecForPolicy(execMsg: ExecServerMessage, hint?: string): Uint8Array {
export function rejectDeleteExecForPolicy(execMsg: ExecServerMessage, hint?: string, plainToolWording = false): 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: `${hint ?? NATIVE_LOCAL_EXEC_DISABLED} No file was changed.` }),
value: create(DeleteRejectedSchema, { path, reason: plainToolWording ? `No file was changed. ${hint ?? NATIVE_LOCAL_EXEC_DISABLED}` : `${hint ?? NATIVE_LOCAL_EXEC_DISABLED} No file was changed.` }),
},
}));
}
Expand Down
16 changes: 12 additions & 4 deletions src/adapters/cursor/native-exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import { OCX_RESPONSES_TOOL_PROVIDER } from "./tool-definitions";
import { CODEX_UNIFIED_EXEC_TOOL, cursorRequestHasExecutionPath, cursorRequestHasShellAlias, cursorRequestUsesCodeMode, cursorToolWireName } from "./tool-naming";
import { CODE_MODE_RESULT_ECHO_SENTENCE } from "../exec-tool-result-normalize";
import type { OcxTool } from "../../types";
import { cursorPlainNativeExecFallback, cursorPlainNativeExecRedirectHint, cursorUsesPlainToolWording } from "./tool-wording";

export type CursorNativeExecDeps = CursorNativeNetworkDeps & CursorNativeToolDeps;

Expand All @@ -80,6 +81,8 @@ export interface CursorNativeExecContext extends CursorNativeExecDeps {
structuredEditAvailable?: boolean;
/** Catalog-aware redirect text for denied native fs/shell attempts (undefined = default bridge wording). */
nativeExecRedirectHint?: string;
/** Claude-family Cursor targets receive factual redirects, without narration restrictions. */
plainToolWording?: boolean;
}

const REDIRECT_HINT_MAX_TOOLS = 16;
Expand All @@ -93,7 +96,9 @@ const REDIRECT_HINT_MAX_TOOLS = 16;
export function cursorNativeExecRedirectHint(
tools: readonly Pick<OcxTool, "namespace" | "name" | "freeform">[] | undefined,
mcpToolDefs: readonly Pick<McpToolDefinition, "name" | "providerIdentifier">[] = [],
modelId?: string,
): string | undefined {
if (cursorUsesPlainToolWording(modelId)) return cursorPlainNativeExecRedirectHint(tools, mcpToolDefs);
const clientTools = tools ?? [];
if (cursorRequestHasShellAlias(clientTools)) return undefined;
// Code mode (freeform unified `exec`, no bare shell bridge): the default bridge wording names
Expand Down Expand Up @@ -704,6 +709,9 @@ export function cursorBlobStoreDebugSnapshotForTests(): Array<{

export async function handleCursorNativeExec(execMsg: ExecServerMessage, deps: CursorNativeExecContext = {}): Promise<Uint8Array[]> {
const execCase = execMsg.message.case;
if (deps.plainToolWording && !deps.nativeExecRedirectHint) {
deps = { ...deps, nativeExecRedirectHint: cursorPlainNativeExecFallback([...(deps.clientToolDefs ?? []), ...(deps.mcpToolDefs ?? [])]) };
}
if (execCase === "requestContextArgs") {
const tools = [...(deps.mcpToolDefs ?? []), ...(deps.clientToolDefs ?? [])];
return [execBytes(execMsg, "requestContextResult", create(RequestContextResultSchema, {
Expand All @@ -712,8 +720,8 @@ export async function handleCursorNativeExec(execMsg: ExecServerMessage, deps: C
}
if (!cursorUnsafeNativeLocalExecEnabled(deps)) {
if (execCase === "readArgs") return [rejectReadExecForPolicy(execMsg, deps.nativeExecRedirectHint)];
if (execCase === "writeArgs") return [rejectWriteExecForPolicy(execMsg, deps.nativeExecRedirectHint)];
if (execCase === "deleteArgs") return [rejectDeleteExecForPolicy(execMsg, deps.nativeExecRedirectHint)];
if (execCase === "writeArgs") return [rejectWriteExecForPolicy(execMsg, deps.nativeExecRedirectHint, deps.plainToolWording)];
if (execCase === "deleteArgs") return [rejectDeleteExecForPolicy(execMsg, deps.nativeExecRedirectHint, deps.plainToolWording)];
if (execCase === "lsArgs") return [rejectLsExecForPolicy(execMsg, deps.nativeExecRedirectHint)];
if (execCase === "grepArgs") return [rejectGrepExecForPolicy(execMsg, deps.nativeExecRedirectHint)];
if (execCase === "shellArgs") return [rejectShellExecForPolicy(execMsg, deps.nativeExecRedirectHint)];
Expand All @@ -723,8 +731,8 @@ export async function handleCursorNativeExec(execMsg: ExecServerMessage, deps: C
if (execCase === "fetchArgs") return [rejectFetchExecForPolicy(execMsg, deps.nativeExecRedirectHint)];
}
if (execCase === "readArgs") return [readExec(execMsg)];
if (execCase === "writeArgs") return [deps.rejectNativeFileMutations ? rejectWriteExecForApplyPatch(execMsg, deps.structuredEditAvailable === true) : writeExec(execMsg)];
if (execCase === "deleteArgs") return [deps.rejectNativeFileMutations ? rejectDeleteExecForApplyPatch(execMsg, deps.structuredEditAvailable === true) : deleteExec(execMsg)];
if (execCase === "writeArgs") return [deps.rejectNativeFileMutations ? rejectWriteExecForApplyPatch(execMsg, deps.structuredEditAvailable === true, deps.plainToolWording) : writeExec(execMsg)];
if (execCase === "deleteArgs") return [deps.rejectNativeFileMutations ? rejectDeleteExecForApplyPatch(execMsg, deps.structuredEditAvailable === true, deps.plainToolWording) : deleteExec(execMsg)];
if (execCase === "lsArgs") return [lsExec(execMsg)];
if (execCase === "grepArgs") return [grepExec(execMsg)];
if (execCase === "shellArgs") return [shellExec(execMsg, deps.nativeExecRedirectHint)];
Expand Down
1 change: 1 addition & 0 deletions src/adapters/cursor/protobuf-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ function systemPromptBlobs(request: CursorRunRequest): RootBlobCandidate[] {
const cursorToolGuidance = buildCursorToolGuidanceSystemNote(
cursorToolsForActivePrompt(request.tools, activePromptText(request), request.toolChoice),
request.toolChoice,
request.modelId,
);
if (cursorToolGuidance) prompts.push(cursorToolGuidance);
return prompts.map(content => rootBlobCandidate({ role: "system", content }, "system"));
Expand Down
Loading
Loading