@yansigit/opencodex 2.33.0 → 2.33.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/index-CIDo4y4k.js +102 -0
- package/gui/dist/index.html +1 -1
- package/package.json +3 -1
- package/src/adapters/command-code.ts +101 -20
- package/src/adapters/cursor/envelope-echo.ts +162 -0
- package/src/adapters/cursor/live-transport.ts +3 -1
- package/src/adapters/cursor/native-exec-fs.ts +13 -12
- package/src/adapters/cursor/native-exec-network.ts +3 -5
- package/src/adapters/cursor/native-exec-policy.ts +47 -0
- package/src/adapters/cursor/native-exec-shell.ts +13 -25
- package/src/adapters/cursor/native-exec.ts +18 -10
- package/src/adapters/cursor/protobuf-events.ts +28 -2
- package/src/adapters/cursor/protobuf-request.ts +20 -3
- package/src/adapters/cursor/request-builder.ts +7 -0
- package/src/adapters/cursor/tool-definitions.ts +22 -1
- package/src/adapters/cursor/tool-result-normalize.ts +21 -8
- package/src/adapters/cursor/types.ts +7 -0
- package/src/adapters/cursor.ts +114 -0
- package/src/adapters/google-aistudio-parser.ts +49 -0
- package/src/adapters/google.ts +108 -16
- package/src/adapters/openai-responses.ts +1 -0
- package/src/chat/inbound.ts +15 -0
- package/src/cli/index.ts +1 -1
- package/src/codex/catalog/provider-fetch.ts +34 -0
- package/src/generated/compatibility-version.json +91 -47
- package/src/generated/model-metadata.ts +3 -0
- package/src/oauth/aistudio-native-daemon.ts +62 -0
- package/src/oauth/aistudio-session-sync.ts +95 -0
- package/src/oauth/google-aistudio-auth.ts +98 -0
- package/src/oauth/key-providers.ts +8 -0
- package/src/oauth/login-cli.ts +66 -1
- package/src/providers/derive.ts +1 -1
- package/src/providers/quota.ts +90 -38
- package/src/providers/registry.ts +24 -3
- package/src/router.ts +3 -0
- package/src/routing/account-pool/cooldown.ts +8 -0
- package/src/routing/account-pool/index.ts +1 -0
- package/src/server/aistudio-ws-hub.ts +295 -0
- package/src/server/auth-cors.ts +1 -0
- package/src/server/chat-completions.ts +2 -0
- package/src/server/index.ts +94 -0
- package/src/server/management/logs-usage-routes.ts +11 -5
- package/src/server/management/oauth-account-routes.ts +13 -3
- package/src/server/port-reclaim.ts +19 -1
- package/src/server/request-log-conversation.ts +12 -0
- package/src/server/request-log.ts +2 -1
- package/src/server/responses/core.ts +4 -3
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/ws-bridge.ts +2 -1
- package/src/smoke/fingerprint-cache.ts +133 -0
- package/src/smoke/live-scenarios.ts +33 -0
- package/src/smoke/runner.ts +119 -0
- package/src/types/provider.ts +2 -1
- package/src/types/request.ts +2 -0
- package/src/types/tools.ts +31 -7
- package/src/usage/command-code-manifest.ts +116 -0
- package/src/usage/cost.ts +2 -2
- package/src/usage/expected-prices.ts +83 -0
- package/src/usage/log.ts +2 -2
- package/src/usage/summary.ts +34 -12
- package/src/web-search/index.ts +16 -8
- package/gui/dist/assets/index-DKLr4LTE.js +0 -102
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
type ExecServerMessage,
|
|
20
20
|
} from "./gen/agent_pb";
|
|
21
21
|
import { errorText, execBytes, execStreamCloseBytes } from "./native-exec-common";
|
|
22
|
+
export { nativeShellDisabledMessage, type CursorNativeExecPolicyContext } from "./native-exec-policy";
|
|
23
|
+
import { nativeShellDisabledMessage, type CursorNativeExecPolicyContext } from "./native-exec-policy";
|
|
22
24
|
import {
|
|
23
25
|
createAdmissionGate,
|
|
24
26
|
type AdmissionLease,
|
|
@@ -81,21 +83,7 @@ let absoluteTerminations = 0;
|
|
|
81
83
|
let unresolvedKills = 0;
|
|
82
84
|
let killFailures = 0;
|
|
83
85
|
|
|
84
|
-
|
|
85
|
-
export function nativeShellDisabledMessage(): string {
|
|
86
|
-
// Do not insist on "the same command" — that steers models into replaying bash/CMD
|
|
87
|
-
// idioms through the Codex bridge on Windows PowerShell 5.1 and looping (#604).
|
|
88
|
-
// Keep this host-shell-neutral: OpenCodex may run on a different OS than the Codex
|
|
89
|
-
// client that executes the bridge (LAN/SSH remote-proxy).
|
|
90
|
-
return (
|
|
91
|
-
"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). "
|
|
92
|
-
+ "Adapt the command for the Codex client host shell before calling the bridge "
|
|
93
|
-
+ "(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 `&&`). "
|
|
94
|
-
+ "Make at most one corrected bridge attempt after a failure, then report the error and stop — do not repeat equivalent failing commands."
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function rejectedShellResult(command: string, cwd: string, started: number) {
|
|
86
|
+
function rejectedShellResult(command: string, cwd: string, started: number, opts: CursorNativeExecPolicyContext = {}) {
|
|
99
87
|
return create(ShellResultSchema, {
|
|
100
88
|
result: {
|
|
101
89
|
case: "failure",
|
|
@@ -105,7 +93,7 @@ function rejectedShellResult(command: string, cwd: string, started: number) {
|
|
|
105
93
|
exitCode: 1,
|
|
106
94
|
signal: "",
|
|
107
95
|
stdout: "",
|
|
108
|
-
stderr: nativeShellDisabledMessage(),
|
|
96
|
+
stderr: nativeShellDisabledMessage(opts),
|
|
109
97
|
executionTime: Date.now() - started,
|
|
110
98
|
aborted: true,
|
|
111
99
|
}),
|
|
@@ -113,10 +101,10 @@ function rejectedShellResult(command: string, cwd: string, started: number) {
|
|
|
113
101
|
});
|
|
114
102
|
}
|
|
115
103
|
|
|
116
|
-
export function rejectShellExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
|
|
104
|
+
export function rejectShellExecForPolicy(execMsg: ExecServerMessage, opts: CursorNativeExecPolicyContext = {}): Uint8Array {
|
|
117
105
|
if (execMsg.message.case !== "shellArgs") throw new Error("invalid shell exec");
|
|
118
106
|
const args = execMsg.message.value;
|
|
119
|
-
return execBytes(execMsg, "shellResult", rejectedShellResult(args.command, resolve(args.workingDirectory || process.cwd()), Date.now()));
|
|
107
|
+
return execBytes(execMsg, "shellResult", rejectedShellResult(args.command, resolve(args.workingDirectory || process.cwd()), Date.now(), opts));
|
|
120
108
|
}
|
|
121
109
|
|
|
122
110
|
export function shellExec(execMsg: ExecServerMessage): Uint8Array {
|
|
@@ -154,7 +142,7 @@ export function shellExec(execMsg: ExecServerMessage): Uint8Array {
|
|
|
154
142
|
}));
|
|
155
143
|
}
|
|
156
144
|
|
|
157
|
-
export function rejectShellStreamExecForPolicy(execMsg: ExecServerMessage): Uint8Array[] {
|
|
145
|
+
export function rejectShellStreamExecForPolicy(execMsg: ExecServerMessage, opts: CursorNativeExecPolicyContext = {}): Uint8Array[] {
|
|
158
146
|
if (execMsg.message.case !== "shellStreamArgs") throw new Error("invalid shell stream exec");
|
|
159
147
|
const args = execMsg.message.value;
|
|
160
148
|
const cwd = resolve(args.workingDirectory || process.cwd());
|
|
@@ -164,12 +152,12 @@ export function rejectShellStreamExecForPolicy(execMsg: ExecServerMessage): Uint
|
|
|
164
152
|
event: { case: "start", value: create(ShellStreamStartSchema, { sandboxPolicy: args.requestedSandboxPolicy }) },
|
|
165
153
|
})),
|
|
166
154
|
execBytes(execMsg, "shellStream", create(ShellStreamSchema, {
|
|
167
|
-
event: { case: "stderr", value: create(ShellStreamStderrSchema, { data: nativeShellDisabledMessage() }) },
|
|
155
|
+
event: { case: "stderr", value: create(ShellStreamStderrSchema, { data: nativeShellDisabledMessage(opts) }) },
|
|
168
156
|
})),
|
|
169
157
|
execBytes(execMsg, "shellStream", create(ShellStreamSchema, {
|
|
170
158
|
event: { case: "exit", value: create(ShellStreamExitSchema, { code: 1, cwd, aborted: true }) },
|
|
171
159
|
})),
|
|
172
|
-
execBytes(execMsg, "shellResult", rejectedShellResult(args.command, cwd, started)),
|
|
160
|
+
execBytes(execMsg, "shellResult", rejectedShellResult(args.command, cwd, started, opts)),
|
|
173
161
|
execStreamCloseBytes(execMsg),
|
|
174
162
|
];
|
|
175
163
|
}
|
|
@@ -260,12 +248,12 @@ export async function shellStreamExec(execMsg: ExecServerMessage): Promise<Uint8
|
|
|
260
248
|
return replies;
|
|
261
249
|
}
|
|
262
250
|
|
|
263
|
-
export function rejectBackgroundShellSpawnExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
|
|
251
|
+
export function rejectBackgroundShellSpawnExecForPolicy(execMsg: ExecServerMessage, opts: CursorNativeExecPolicyContext = {}): Uint8Array {
|
|
264
252
|
if (execMsg.message.case !== "backgroundShellSpawnArgs") throw new Error("invalid background shell exec");
|
|
265
253
|
const args = execMsg.message.value;
|
|
266
254
|
const cwd = resolve(args.workingDirectory || process.cwd());
|
|
267
255
|
return execBytes(execMsg, "backgroundShellSpawnResult", create(BackgroundShellSpawnResultSchema, {
|
|
268
|
-
result: { case: "error", value: create(BackgroundShellSpawnErrorSchema, { command: args.command, workingDirectory: cwd, error: nativeShellDisabledMessage() }) },
|
|
256
|
+
result: { case: "error", value: create(BackgroundShellSpawnErrorSchema, { command: args.command, workingDirectory: cwd, error: nativeShellDisabledMessage(opts) }) },
|
|
269
257
|
}));
|
|
270
258
|
}
|
|
271
259
|
|
|
@@ -517,10 +505,10 @@ export function backgroundShellSpawnExec(execMsg: ExecServerMessage, sessionId:
|
|
|
517
505
|
}
|
|
518
506
|
}
|
|
519
507
|
|
|
520
|
-
export function rejectWriteShellStdinExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
|
|
508
|
+
export function rejectWriteShellStdinExecForPolicy(execMsg: ExecServerMessage, opts: CursorNativeExecPolicyContext = {}): Uint8Array {
|
|
521
509
|
if (execMsg.message.case !== "writeShellStdinArgs") throw new Error("invalid shell stdin exec");
|
|
522
510
|
return execBytes(execMsg, "writeShellStdinResult", create(WriteShellStdinResultSchema, {
|
|
523
|
-
result: { case: "error", value: create(WriteShellStdinErrorSchema, { error: nativeShellDisabledMessage() }) },
|
|
511
|
+
result: { case: "error", value: create(WriteShellStdinErrorSchema, { error: nativeShellDisabledMessage(opts) }) },
|
|
524
512
|
}));
|
|
525
513
|
}
|
|
526
514
|
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
type CursorNativeToolDeps,
|
|
52
52
|
} from "./native-exec-tools";
|
|
53
53
|
import { clientBytes, execBytes, execStreamCloseBytes, execThrowBytes } from "./native-exec-common";
|
|
54
|
+
import { type CursorNativeExecPolicyContext } from "./native-exec-policy";
|
|
54
55
|
import type { McpToolDefinition } from "./gen/agent_pb";
|
|
55
56
|
import { OCX_RESPONSES_TOOL_PROVIDER } from "./tool-definitions";
|
|
56
57
|
|
|
@@ -72,12 +73,18 @@ export interface CursorNativeExecContext extends CursorNativeExecDeps {
|
|
|
72
73
|
rejectNativeFileMutations?: boolean;
|
|
73
74
|
/** The synthetic exact-match edit tools (edit_file / multi_edit) are advertised this request. */
|
|
74
75
|
structuredEditAvailable?: boolean;
|
|
76
|
+
/** Codex code mode advertises one freeform exec tool; native rejections must steer to nested helpers. */
|
|
77
|
+
codeMode?: boolean;
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
export function cursorUnsafeNativeLocalExecEnabled(input: Pick<CursorNativeExecContext, "unsafeAllowNativeLocalExec"> = {}): boolean {
|
|
78
81
|
return input.unsafeAllowNativeLocalExec === true;
|
|
79
82
|
}
|
|
80
83
|
|
|
84
|
+
function nativeExecPolicyContext(deps: CursorNativeExecContext): CursorNativeExecPolicyContext {
|
|
85
|
+
return deps.codeMode === true ? { codeMode: true } : {};
|
|
86
|
+
}
|
|
87
|
+
|
|
81
88
|
/**
|
|
82
89
|
* Content-addressed blob store shared across streams. Bounded: without eviction a long-running
|
|
83
90
|
* proxy accumulates every conversation's prompt blobs forever (unbounded memory) and any stale
|
|
@@ -556,16 +563,17 @@ export async function handleCursorNativeExec(execMsg: ExecServerMessage, deps: C
|
|
|
556
563
|
}))];
|
|
557
564
|
}
|
|
558
565
|
if (!cursorUnsafeNativeLocalExecEnabled(deps)) {
|
|
559
|
-
|
|
560
|
-
if (execCase === "
|
|
561
|
-
if (execCase === "
|
|
562
|
-
if (execCase === "
|
|
563
|
-
if (execCase === "
|
|
564
|
-
if (execCase === "
|
|
565
|
-
if (execCase === "
|
|
566
|
-
if (execCase === "
|
|
567
|
-
if (execCase === "
|
|
568
|
-
if (execCase === "
|
|
566
|
+
const policy = nativeExecPolicyContext(deps);
|
|
567
|
+
if (execCase === "readArgs") return [rejectReadExecForPolicy(execMsg, policy)];
|
|
568
|
+
if (execCase === "writeArgs") return [rejectWriteExecForPolicy(execMsg, policy)];
|
|
569
|
+
if (execCase === "deleteArgs") return [rejectDeleteExecForPolicy(execMsg, policy)];
|
|
570
|
+
if (execCase === "lsArgs") return [rejectLsExecForPolicy(execMsg, policy)];
|
|
571
|
+
if (execCase === "grepArgs") return [rejectGrepExecForPolicy(execMsg, policy)];
|
|
572
|
+
if (execCase === "shellArgs") return [rejectShellExecForPolicy(execMsg, policy)];
|
|
573
|
+
if (execCase === "shellStreamArgs") return rejectShellStreamExecForPolicy(execMsg, policy);
|
|
574
|
+
if (execCase === "backgroundShellSpawnArgs") return [rejectBackgroundShellSpawnExecForPolicy(execMsg, policy)];
|
|
575
|
+
if (execCase === "writeShellStdinArgs") return [rejectWriteShellStdinExecForPolicy(execMsg, policy)];
|
|
576
|
+
if (execCase === "fetchArgs") return [rejectFetchExecForPolicy(execMsg, policy)];
|
|
569
577
|
}
|
|
570
578
|
if (execCase === "readArgs") return [readExec(execMsg)];
|
|
571
579
|
if (execCase === "writeArgs") return [deps.rejectNativeFileMutations ? rejectWriteExecForApplyPatch(execMsg, deps.structuredEditAvailable === true) : writeExec(execMsg)];
|
|
@@ -1136,6 +1136,31 @@ function cursorFreeformWrapperValid(args: string): boolean {
|
|
|
1136
1136
|
}
|
|
1137
1137
|
}
|
|
1138
1138
|
|
|
1139
|
+
/**
|
|
1140
|
+
* Cursor sometimes sends a freeform body as a JSON string or as a one-key object instead of the
|
|
1141
|
+
* advertised `{input: string}` wrapper. Preserve incomplete JSON for the late native-args path,
|
|
1142
|
+
* but wrap complete/raw bodies before the Responses bridge validates them.
|
|
1143
|
+
*/
|
|
1144
|
+
function normalizeFreeformArgs(args: string): string {
|
|
1145
|
+
if (args.length === 0) return args;
|
|
1146
|
+
try {
|
|
1147
|
+
const parsed = JSON.parse(args) as unknown;
|
|
1148
|
+
if (typeof parsed === "string") return JSON.stringify({ input: parsed });
|
|
1149
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1150
|
+
const object = parsed as Record<string, unknown>;
|
|
1151
|
+
if ("input" in object || Object.keys(object).length === 0) return args;
|
|
1152
|
+
const values = Object.values(object);
|
|
1153
|
+
const input = values.length === 1 && typeof values[0] === "string" ? values[0] : JSON.stringify(object);
|
|
1154
|
+
return JSON.stringify({ input });
|
|
1155
|
+
}
|
|
1156
|
+
return args;
|
|
1157
|
+
} catch {
|
|
1158
|
+
return args.trimStart().startsWith("{") || args.trimStart().startsWith("[")
|
|
1159
|
+
? args
|
|
1160
|
+
: JSON.stringify({ input: args });
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1139
1164
|
function dropInvalidFreeformCall(state: CursorProtobufEventState, callId: string, toolName: string): CursorServerMessage[] {
|
|
1140
1165
|
state.openToolCalls.delete(callId);
|
|
1141
1166
|
state.translatorBudget?.closeCall(callId);
|
|
@@ -1162,6 +1187,7 @@ function dropStructuredEditCall(state: CursorProtobufEventState, callId: string,
|
|
|
1162
1187
|
function commitToolCall(state: CursorProtobufEventState, callId: string, finalArgs: string): CursorServerMessage[] {
|
|
1163
1188
|
const open = state.openToolCalls.get(callId);
|
|
1164
1189
|
if (!open) return [];
|
|
1190
|
+
if (state.freeformToolNames?.has(open.name)) finalArgs = normalizeFreeformArgs(finalArgs);
|
|
1165
1191
|
if (state.freeformToolNames?.has(open.name) && !cursorFreeformWrapperValid(finalArgs)) {
|
|
1166
1192
|
return dropInvalidFreeformCall(state, callId, open.name);
|
|
1167
1193
|
}
|
|
@@ -1436,7 +1462,7 @@ function parseCursorTextToolCalls(text: string, state: CursorProtobufEventState)
|
|
|
1436
1462
|
&& (
|
|
1437
1463
|
(name !== undefined && openBeforeStart.args.length === 0)
|
|
1438
1464
|
|| (state.freeformToolNames?.has(openBeforeStart.name) === true
|
|
1439
|
-
&& !cursorFreeformWrapperValid(openBeforeStart.args)
|
|
1465
|
+
&& !cursorFreeformWrapperValid(normalizeFreeformArgs(openBeforeStart.args))
|
|
1440
1466
|
)
|
|
1441
1467
|
)
|
|
1442
1468
|
) {
|
|
@@ -1460,7 +1486,7 @@ function parseCursorTextToolCalls(text: string, state: CursorProtobufEventState)
|
|
|
1460
1486
|
if (
|
|
1461
1487
|
!openBeforeStart && open && !hasMcpArgBytes(args)
|
|
1462
1488
|
&& state.freeformToolNames?.has(open.name) === true
|
|
1463
|
-
&& !cursorFreeformWrapperValid(open.args)
|
|
1489
|
+
&& !cursorFreeformWrapperValid(normalizeFreeformArgs(open.args))
|
|
1464
1490
|
) {
|
|
1465
1491
|
open.awaitingNativeArgs = true;
|
|
1466
1492
|
return [];
|
|
@@ -77,7 +77,22 @@ export const CURSOR_EXTERNAL_ROOT_BYTE_LIMIT = 512 * 1024;
|
|
|
77
77
|
* results already stored in history blobs are visible without a ResumeAction.
|
|
78
78
|
*/
|
|
79
79
|
export const CURSOR_EXTERNAL_TOOL_CONTINUATION_TEXT =
|
|
80
|
-
"Continue: the requested tool results are provided in the conversation history above.";
|
|
80
|
+
"Continue: the requested tool results are provided in the conversation history above. Answer the user request or proceed with the next step directly without repeating status summaries or greetings.";
|
|
81
|
+
|
|
82
|
+
export function externalToolContinuationText(rawMessages?: readonly OcxMessage[]): string {
|
|
83
|
+
const last = rawMessages?.at(-1);
|
|
84
|
+
if (last?.role === "toolResult") {
|
|
85
|
+
const raw = typeof last.content === "string" ? last.content : JSON.stringify(last.content ?? "");
|
|
86
|
+
const trimmed = raw.trim();
|
|
87
|
+
if (
|
|
88
|
+
(last.toolName?.includes("list_agents") || last.toolName?.includes("search"))
|
|
89
|
+
&& (trimmed === "[]" || trimmed === "" || trimmed === "{}" || trimmed === "null")
|
|
90
|
+
) {
|
|
91
|
+
return `${CURSOR_EXTERNAL_TOOL_CONTINUATION_TEXT} If a prior discovery or list tool returned empty results (e.g. no sub-agents currently active), proceed directly with your next concrete action using available tools.`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return CURSOR_EXTERNAL_TOOL_CONTINUATION_TEXT;
|
|
95
|
+
}
|
|
81
96
|
|
|
82
97
|
/** Runtime timezone for protobuf RequestContextEnv (dynamic, never hardcoded). */
|
|
83
98
|
function runtimeTimeZone(): string {
|
|
@@ -855,8 +870,10 @@ function buildPreparedCursorRunRequest(
|
|
|
855
870
|
? "userMessageAction"
|
|
856
871
|
: "resumeAction";
|
|
857
872
|
const actionText = externalToolContinuation
|
|
858
|
-
?
|
|
859
|
-
:
|
|
873
|
+
? (request.echoRetryContinuationText ?? externalToolContinuationText(request.rawMessages))
|
|
874
|
+
: request.echoRetryContinuationText
|
|
875
|
+
? `${text}\n\n[correction] ${request.echoRetryContinuationText}`
|
|
876
|
+
: text;
|
|
860
877
|
const action = create(ConversationActionSchema, {
|
|
861
878
|
action: actionCase === "userMessageAction"
|
|
862
879
|
? {
|
|
@@ -202,6 +202,13 @@ function normalizeCursorModelId(modelId: string, reasoning?: string): {
|
|
|
202
202
|
],
|
|
203
203
|
};
|
|
204
204
|
}
|
|
205
|
+
if (id === "composer-2.5") {
|
|
206
|
+
return {
|
|
207
|
+
...selection,
|
|
208
|
+
modelId: id,
|
|
209
|
+
requestedModelParameters: [{ id: "fast", value: "false" }],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
205
212
|
return { ...selection, modelId: suffix ? cursorRequestWireModelIdWithEffort(id, suffix) : id };
|
|
206
213
|
}
|
|
207
214
|
|
|
@@ -112,6 +112,13 @@ export const CURSOR_MULTI_EDIT_INPUT_SCHEMA = {
|
|
|
112
112
|
additionalProperties: false,
|
|
113
113
|
} as const;
|
|
114
114
|
|
|
115
|
+
/** Cursor requires freeform custom tools to advertise their body as one string input. */
|
|
116
|
+
export const CURSOR_FREEFORM_INPUT_SCHEMA = {
|
|
117
|
+
type: "object",
|
|
118
|
+
properties: { input: { type: "string" } },
|
|
119
|
+
required: ["input"],
|
|
120
|
+
} as const;
|
|
121
|
+
|
|
115
122
|
/**
|
|
116
123
|
* Responses/Codex-side schema used ONLY for arg-key normalization after Cursor returns a call.
|
|
117
124
|
* Cursor models are trained to emit `cmd`; Codex `shell_command` / `exec_command` validate
|
|
@@ -386,6 +393,7 @@ export function responsesToolNameFromCursorWire(name: string, cursorToolNameMap?
|
|
|
386
393
|
|
|
387
394
|
/** Schema advertised to Cursor for this tool (may use Cursor-preferred field names like `cmd`). */
|
|
388
395
|
export function cursorToolInputSchema(tool: OcxTool): unknown {
|
|
396
|
+
if (tool.freeform) return CURSOR_FREEFORM_INPUT_SCHEMA;
|
|
389
397
|
return isBareCodexExecCommandTool(tool) ? CURSOR_EXEC_COMMAND_INPUT_SCHEMA : (tool.parameters ?? {});
|
|
390
398
|
}
|
|
391
399
|
|
|
@@ -395,6 +403,7 @@ export function cursorToolInputSchema(tool: OcxTool): unknown {
|
|
|
395
403
|
* treating `cmd` as canonical prevents the `cmd` → `command` rewrite Codex requires (#399).
|
|
396
404
|
*/
|
|
397
405
|
export function cursorToolArgNormalizeSchema(tool: OcxTool): unknown {
|
|
406
|
+
if (tool.freeform) return CURSOR_FREEFORM_INPUT_SCHEMA;
|
|
398
407
|
if (isBareCodexShellBridgeTool(tool)) {
|
|
399
408
|
return shellBridgeArgNormalizeSchema(tool);
|
|
400
409
|
}
|
|
@@ -678,6 +687,12 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
678
687
|
codeMode
|
|
679
688
|
? "In code mode the isolate returns nothing on its own: call `text(...)` (or `notify(...)`) on any value you need to see, or the call completes with empty output. There is no `require`, no `module`, and no filesystem or network globals; reach the host only through the nested helpers."
|
|
680
689
|
: undefined,
|
|
690
|
+
codeMode
|
|
691
|
+
? "To read, search, or inspect files or run CLI commands, call `await tools.exec_command({ cmd: '...' })` inside `exec`. Do not write Node.js scripts or `require('fs')`."
|
|
692
|
+
: undefined,
|
|
693
|
+
codeMode
|
|
694
|
+
? "NEVER attempt Cursor-native Shell, Read, Grep, List, or any tool absent from the catalog — they are not executed in this environment and every probe wastes a turn. The exec code cell (with its nested helpers) is the ONLY execution surface; go to it directly on the FIRST attempt and do not narrate switching surfaces."
|
|
695
|
+
: undefined,
|
|
681
696
|
hasBareExec
|
|
682
697
|
? `${shellBridgeLabel} is the Codex Responses shell bridge for this turn, exposed through Cursor's tool protocol; it is not an external MCP server tool. \`shell_command\` and \`exec_command\` are aliases of the same bridge.`
|
|
683
698
|
: undefined,
|
|
@@ -685,7 +700,10 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
685
700
|
? "Your tool list may display it under a longer `mcp_opencodex-responses_shell_command` / `mcp_opencodex-responses_exec_command` name; those are the SAME tool — call whichever your list shows, and do not comment on the naming difference to the user."
|
|
686
701
|
: undefined,
|
|
687
702
|
hasBareExec
|
|
688
|
-
? `
|
|
703
|
+
? `NEVER attempt Cursor-native Shell, Read, Grep, List, or any tool not in the catalog above — they are not executed locally in this environment and every attempt wastes a turn and can stall the session. ${shellBridgeLabel} is the ONLY shell surface; go to it directly on the FIRST attempt, never as a fallback after probing a native tool. Do not narrate switching surfaces ("native is blocked, using the bridge instead") — there is exactly one surface.`
|
|
704
|
+
: undefined,
|
|
705
|
+
hasBareExec
|
|
706
|
+
? "Tool-selection commentary is forbidden: for any shell, read, grep, list, or file operation, your FIRST visible action is the bridge call itself — never a sentence about which tool you will use, which tool was redirected, or switching surfaces. Words like 차단/전환/blocked/switching must not appear in your output for tool-routing reasons."
|
|
689
707
|
: undefined,
|
|
690
708
|
hasBareExec
|
|
691
709
|
? 'When a command requires network access, file writes outside workspace, or fails due to sandbox restrictions, include `sandbox_permissions: "require_escalated"` and `justification: "..."`.'
|
|
@@ -700,6 +718,9 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
700
718
|
? `For file edits, prefer the structured edit tools ${quotedNames(structuredEditNames)} — they take replacements that OpenCodex converts into Codex \`apply_patch\` changes. Include exact leading whitespace in old_string/new_string. Use \`apply_patch\` directly only with a \`*** Begin Patch\` envelope and bare \`@@\` hunks (never git-style \`@@ -n,m +n,m @@\`); never emit patch-like plain text as tool arguments.`
|
|
701
719
|
: "For file edits, use the `apply_patch` tool, not built-in file write/delete tools."
|
|
702
720
|
: undefined,
|
|
721
|
+
hasApplyPatch
|
|
722
|
+
? "Creating or modifying file CONTENT via shell redirection (`>`, `>>`, `printf`/`echo` into a file, `cat <<EOF`, `sed -i`) is forbidden while apply_patch or the structured edit tools are advertised — use those edit tools so the change is reviewable. Shell output redirection is fine for logs/scratch pipes that are not the deliverable file."
|
|
723
|
+
: undefined,
|
|
703
724
|
hasBareExec
|
|
704
725
|
? "For tool-count demos, each counted tool must be a separate Codex shell-bridge invocation/result; do not collapse several requested tools into one chained shell command."
|
|
705
726
|
: undefined,
|
|
@@ -44,8 +44,24 @@ const RUNTIME_FAILURE_GUIDANCE: ReadonlyArray<{ marker: string; guidance: string
|
|
|
44
44
|
guidance: "The node_repl session keeps earlier declarations; rename the variable or use var/reassignment instead of redeclaring.",
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
marker: "unsupported import in exec",
|
|
48
|
+
guidance: "Imports are not available in this exec context; use the injected globals instead.",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
marker: "require is not defined",
|
|
52
|
+
guidance: "In Codex code-mode exec (a V8 isolate, not Node.js), require/fs/process are not available. Use await tools.exec_command({ cmd: '...' }) inside exec to inspect files or run CLI tools.",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
marker: "fs is not defined",
|
|
56
|
+
guidance: "In Codex code-mode exec (a V8 isolate, not Node.js), require/fs/process are not available. Use await tools.exec_command({ cmd: '...' }) inside exec to inspect files or run CLI tools.",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
marker: "process is not defined",
|
|
60
|
+
guidance: "In Codex code-mode exec (a V8 isolate, not Node.js), require/fs/process are not available. Use await tools.exec_command({ cmd: '...' }) inside exec to inspect files or run CLI tools.",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
marker: "module is not defined",
|
|
64
|
+
guidance: "In Codex code-mode exec (a V8 isolate, not Node.js), require/fs/process are not available. Use await tools.exec_command({ cmd: '...' }) inside exec to inspect files or run CLI tools.",
|
|
49
65
|
},
|
|
50
66
|
];
|
|
51
67
|
|
|
@@ -80,13 +96,10 @@ export function normalizeCursorToolResultText(
|
|
|
80
96
|
changed: true,
|
|
81
97
|
};
|
|
82
98
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return { text: `${text}\n[recovery: ${guidance}]`, isError: true, changed: true };
|
|
87
|
-
}
|
|
99
|
+
for (const { marker, guidance } of RUNTIME_FAILURE_GUIDANCE) {
|
|
100
|
+
if (text.includes(marker) && !text.includes("[recovery:") && !text.includes(guidance)) {
|
|
101
|
+
return { text: `${text}\n[recovery: ${guidance}]`, isError: true, changed: true };
|
|
88
102
|
}
|
|
89
103
|
}
|
|
90
104
|
return { text, isError, changed: false };
|
|
91
105
|
}
|
|
92
|
-
|
|
@@ -15,6 +15,13 @@ export interface CursorRunRequest {
|
|
|
15
15
|
requestedModelParameters?: readonly CursorRequestedModelParameter[];
|
|
16
16
|
/** Cursor Router optimization parameter; valid only while modelId is the `default` wire model. */
|
|
17
17
|
routingLevel?: CursorRoutingLevel;
|
|
18
|
+
/**
|
|
19
|
+
* Corrective active-turn text for the single envelope-echo retry (devlog 260826 gap-10).
|
|
20
|
+
* When set on an external tool-result continuation, buildPreparedCursorRunRequest uses it as
|
|
21
|
+
* the userMessageAction text instead of the standard continuation text; rawMessages stay
|
|
22
|
+
* untouched so history replay is unchanged.
|
|
23
|
+
*/
|
|
24
|
+
echoRetryContinuationText?: string;
|
|
18
25
|
conversationId: string;
|
|
19
26
|
system: string[];
|
|
20
27
|
messages: CursorRequestMessage[];
|
package/src/adapters/cursor.ts
CHANGED
|
@@ -41,6 +41,15 @@ import {
|
|
|
41
41
|
shouldSurfaceCursorOverflowFirst,
|
|
42
42
|
} from "./cursor/thread-continuity";
|
|
43
43
|
import { runCursorTurnWithRetry } from "./cursor/transport-retry";
|
|
44
|
+
import { cursorRequestHasShellAlias, cursorRequestUsesCodeMode } from "./cursor/tool-definitions";
|
|
45
|
+
import {
|
|
46
|
+
CURSOR_ECHO_RETRY_CONTINUATION_TEXT,
|
|
47
|
+
CURSOR_ROUTING_COMMENTARY_RETRY_TEXT,
|
|
48
|
+
CursorEnvelopeEchoSniffer,
|
|
49
|
+
CursorRoutingCommentaryError,
|
|
50
|
+
CursorRoutingCommentarySniffer,
|
|
51
|
+
CursorToolResultEchoError,
|
|
52
|
+
} from "./cursor/envelope-echo";
|
|
44
53
|
import {
|
|
45
54
|
createDisabledCursorTransport,
|
|
46
55
|
CursorTransportDisabledError,
|
|
@@ -216,6 +225,30 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
216
225
|
|| activeRequest.modelId.includes("kimi-k3")
|
|
217
226
|
|| activeRequest.modelId.includes("opus-4-8");
|
|
218
227
|
const heartbeatOnlyMs = isHeavyReasoning ? 300_000 : 180_000;
|
|
228
|
+
const armEchoSniffer =
|
|
229
|
+
isCursorExternalWireModel(activeRequest.modelId)
|
|
230
|
+
&& (_parsed.context.messages ?? []).some(message => message.role === "toolResult");
|
|
231
|
+
const echoSniffer = armEchoSniffer ? new CursorEnvelopeEchoSniffer() : undefined;
|
|
232
|
+
const armRoutingCommentarySniffer =
|
|
233
|
+
isCursorExternalWireModel(activeRequest.modelId)
|
|
234
|
+
&& (
|
|
235
|
+
cursorRequestUsesCodeMode(activeRequest.tools, activeRequest.toolChoice)
|
|
236
|
+
|| cursorRequestHasShellAlias(activeRequest.tools)
|
|
237
|
+
);
|
|
238
|
+
const routingCommentarySniffer = armRoutingCommentarySniffer
|
|
239
|
+
? new CursorRoutingCommentarySniffer()
|
|
240
|
+
: undefined;
|
|
241
|
+
let guardHeld: AdapterEvent[] = [];
|
|
242
|
+
const releaseGuardHeld = () => {
|
|
243
|
+
for (const held of guardHeld) {
|
|
244
|
+
if (held.type !== "heartbeat") emittedOutput = true;
|
|
245
|
+
emit(held);
|
|
246
|
+
}
|
|
247
|
+
guardHeld = [];
|
|
248
|
+
};
|
|
249
|
+
const guardsSettled = () =>
|
|
250
|
+
(!echoSniffer || echoSniffer.settled)
|
|
251
|
+
&& (!routingCommentarySniffer || routingCommentarySniffer.settled);
|
|
219
252
|
await runCursorTurnWithRetry(
|
|
220
253
|
makeTransport,
|
|
221
254
|
{
|
|
@@ -246,6 +279,43 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
246
279
|
},
|
|
247
280
|
});
|
|
248
281
|
for (const event of events) {
|
|
282
|
+
if (!guardsSettled()) {
|
|
283
|
+
if (event.type === "text_delta") {
|
|
284
|
+
guardHeld.push(event);
|
|
285
|
+
if (echoSniffer && !echoSniffer.settled) {
|
|
286
|
+
const decision = echoSniffer.feed(event.text);
|
|
287
|
+
if (decision.kind === "echo") {
|
|
288
|
+
guardHeld = [];
|
|
289
|
+
throw new CursorToolResultEchoError(decision.marker);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (routingCommentarySniffer && !routingCommentarySniffer.settled) {
|
|
293
|
+
const decision = routingCommentarySniffer.feed(event.text);
|
|
294
|
+
if (decision.kind === "hallucination") {
|
|
295
|
+
guardHeld = [];
|
|
296
|
+
throw new CursorRoutingCommentaryError();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (guardsSettled()) releaseGuardHeld();
|
|
300
|
+
continue;
|
|
301
|
+
} else if (event.type === "thinking_delta" || event.type === "heartbeat") {
|
|
302
|
+
// Reasoning before first text stays ordered; liveness still passes through.
|
|
303
|
+
if (event.type === "thinking_delta") {
|
|
304
|
+
guardHeld.push(event);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
// A tool call, done, or error closes the text quarantine. Re-check the full
|
|
309
|
+
// held first line before release so a fragmented routing claim cannot leak.
|
|
310
|
+
echoSniffer?.finish();
|
|
311
|
+
const routingDecision = routingCommentarySniffer?.finish();
|
|
312
|
+
if (routingDecision?.kind === "hallucination") {
|
|
313
|
+
guardHeld = [];
|
|
314
|
+
throw new CursorRoutingCommentaryError();
|
|
315
|
+
}
|
|
316
|
+
releaseGuardHeld();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
249
319
|
if (event.type !== "heartbeat") emittedOutput = true;
|
|
250
320
|
if (event.type === "done") {
|
|
251
321
|
commitCapturedCheckpoint(activeRequest);
|
|
@@ -299,6 +369,50 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
299
369
|
await runOnce(request);
|
|
300
370
|
break;
|
|
301
371
|
} catch (err) {
|
|
372
|
+
const outputGuardRetryText =
|
|
373
|
+
err instanceof CursorToolResultEchoError
|
|
374
|
+
? CURSOR_ECHO_RETRY_CONTINUATION_TEXT
|
|
375
|
+
: err instanceof CursorRoutingCommentaryError
|
|
376
|
+
? CURSOR_ROUTING_COMMENTARY_RETRY_TEXT
|
|
377
|
+
: undefined;
|
|
378
|
+
// One-shot corrective retry for guarded external output (devlog 260826 gap-10/11).
|
|
379
|
+
// The quarantine guarantees no client-visible delta escaped, so a fresh-conversation
|
|
380
|
+
// retry is safe. A second rejection propagates as an error rather than looping.
|
|
381
|
+
if (
|
|
382
|
+
outputGuardRetryText
|
|
383
|
+
&& !emittedOutput
|
|
384
|
+
&& !replayUnsafe
|
|
385
|
+
&& !incoming.abortSignal?.aborted
|
|
386
|
+
) {
|
|
387
|
+
debugProviderDiagnostic(
|
|
388
|
+
"cursor",
|
|
389
|
+
err instanceof CursorToolResultEchoError
|
|
390
|
+
? "envelope-echo-retry"
|
|
391
|
+
: "routing-commentary-retry",
|
|
392
|
+
{
|
|
393
|
+
wireModel: request.modelId,
|
|
394
|
+
conversationHash: request.conversationId.slice(0, 16),
|
|
395
|
+
},
|
|
396
|
+
);
|
|
397
|
+
const echoedConversationId = request.conversationId;
|
|
398
|
+
lastTransport = undefined;
|
|
399
|
+
_parsed._cursorConversationId = undefined;
|
|
400
|
+
request = {
|
|
401
|
+
...createCursorRequest(_parsed, { forceFreshConversation: true }),
|
|
402
|
+
echoRetryContinuationText: outputGuardRetryText,
|
|
403
|
+
};
|
|
404
|
+
rekeyContextUsage(echoedConversationId, request.conversationId);
|
|
405
|
+
_parsed._cursorConversationId = request.conversationId;
|
|
406
|
+
if (_parsed._clientThreadId && _parsed._cursorIsolateConversation !== true) {
|
|
407
|
+
rememberCursorThreadConversation(
|
|
408
|
+
_parsed._clientThreadId,
|
|
409
|
+
request.conversationId,
|
|
410
|
+
_parsed._cursorIdentityScope,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
await runOnce(request);
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
302
416
|
const overflowRemintSafe =
|
|
303
417
|
!lastRawIsToolResult
|
|
304
418
|
&& !emittedOutput
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resilient streaming chunk parser for Google AI Studio MakerSuite private RPC protocol.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface MakerSuiteParsedResult {
|
|
6
|
+
text: string;
|
|
7
|
+
thought?: string;
|
|
8
|
+
thoughtSignature?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function parseMakerSuiteChunk(raw: string): MakerSuiteParsedResult {
|
|
12
|
+
const result: MakerSuiteParsedResult = { text: "" };
|
|
13
|
+
if (!raw || typeof raw !== "string") return result;
|
|
14
|
+
|
|
15
|
+
// 1. Extract regular model response text: [null, "escaped_text"]
|
|
16
|
+
const textMatches = raw.matchAll(/\[\s*null\s*,\s*"((?:[^"\\]|\\.)*)"/g);
|
|
17
|
+
for (const m of textMatches) {
|
|
18
|
+
if (!m[1]) continue;
|
|
19
|
+
try {
|
|
20
|
+
const decoded = JSON.parse('"' + m[1] + '"');
|
|
21
|
+
if (decoded) result.text += decoded;
|
|
22
|
+
} catch (err) {
|
|
23
|
+
void err;
|
|
24
|
+
result.text += m[1];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 2. Extract model reasoning / thoughts: [true, "escaped_thought"]
|
|
29
|
+
const thoughtMatches = raw.matchAll(/\[\s*true\s*,\s*"((?:[^"\\]|\\.)*)"/g);
|
|
30
|
+
for (const m of thoughtMatches) {
|
|
31
|
+
if (!m[1]) continue;
|
|
32
|
+
try {
|
|
33
|
+
const decoded = JSON.parse('"' + m[1] + '"');
|
|
34
|
+
if (decoded) result.thought = (result.thought ?? "") + decoded;
|
|
35
|
+
} catch (err) {
|
|
36
|
+
void err;
|
|
37
|
+
result.thought = (result.thought ?? "") + m[1];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 3. Extract Gemini thought signature token if present
|
|
42
|
+
const sigMatch = raw.match(/"(ErQ[A-Za-z0-9+/=_-]{30,})"/);
|
|
43
|
+
if (sigMatch?.[1]) {
|
|
44
|
+
result.thoughtSignature = sigMatch[1];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|