@yansigit/opencodex 2.32.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/README.md +2 -2
- package/gui/dist/assets/index-CIDo4y4k.js +102 -0
- package/gui/dist/assets/index-DrSQdTRd.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +8 -5
- package/src/adapters/anthropic.ts +20 -6
- package/src/adapters/azure.ts +20 -4
- package/src/adapters/base.ts +3 -1
- package/src/adapters/command-code.ts +131 -17
- package/src/adapters/cursor/envelope-echo.ts +162 -0
- package/src/adapters/cursor/live-transport.ts +4 -2
- 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 +186 -9
- package/src/adapters/cursor/protobuf-request.ts +53 -18
- package/src/adapters/cursor/request-builder.ts +11 -3
- package/src/adapters/cursor/tool-definitions.ts +49 -2
- package/src/adapters/cursor/tool-result-normalize.ts +21 -8
- package/src/adapters/cursor/types.ts +11 -3
- package/src/adapters/cursor.ts +123 -0
- package/src/adapters/google-aistudio-parser.ts +49 -0
- package/src/adapters/google-antigravity-replay.ts +2 -2
- package/src/adapters/google-antigravity-wire.ts +7 -0
- package/src/adapters/google-errors.ts +6 -2
- package/src/adapters/google-http.ts +30 -7
- package/src/adapters/google-truncation.ts +5 -0
- package/src/adapters/google-wire-compiler.ts +38 -6
- package/src/adapters/google.ts +242 -28
- package/src/adapters/kiro-tools.ts +20 -9
- package/src/adapters/openai-chat.ts +9 -0
- package/src/adapters/openai-responses.ts +2 -1
- package/src/bridge.ts +112 -9
- package/src/chat/inbound.ts +15 -0
- package/src/claude/context-windows.ts +16 -9
- package/src/cli/doctor.ts +2 -2
- package/src/cli/index.ts +10 -3
- package/src/cli/provider.ts +6 -0
- package/src/cli/status.ts +23 -0
- package/src/codex/auth-api.ts +4 -2
- package/src/codex/autostart-health.ts +16 -0
- package/src/codex/catalog/aggregation.ts +12 -12
- package/src/codex/catalog/effort.ts +18 -3
- package/src/codex/catalog/metadata.ts +27 -1
- package/src/codex/catalog/model-metadata.ts +39 -12
- package/src/codex/catalog/parsing.ts +38 -27
- package/src/codex/catalog/provider-fetch.ts +232 -133
- package/src/codex/catalog/sync.ts +1 -1
- package/src/codex/convergence.ts +5 -0
- package/src/codex/shim.ts +56 -3
- package/src/config/provider-validation.ts +37 -0
- package/src/config.ts +78 -2
- package/src/generated/compatibility-version.json +187 -119
- package/src/generated/model-metadata.ts +3 -0
- package/src/images/loop.ts +37 -6
- package/src/lib/azure-identity.ts +154 -0
- package/src/lib/debug.ts +42 -0
- package/src/lib/errors.ts +14 -0
- package/src/lib/provider-outbound.ts +45 -33
- package/src/lib/provider-tls-profile.ts +309 -0
- package/src/lib/proxy-env.ts +49 -0
- package/src/lib/redact.ts +10 -1
- package/src/oauth/aistudio-native-daemon.ts +62 -0
- package/src/oauth/aistudio-session-sync.ts +95 -0
- package/src/oauth/antigravity-routing.ts +282 -236
- package/src/oauth/callback-server.ts +22 -2
- package/src/oauth/command-code.ts +5 -16
- package/src/oauth/google-aistudio-auth.ts +98 -0
- package/src/oauth/google-antigravity.ts +42 -5
- package/src/oauth/index.ts +15 -3
- package/src/oauth/key-providers.ts +8 -0
- package/src/oauth/kimi.ts +9 -1
- package/src/oauth/login-cli.ts +66 -1
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +6 -0
- package/src/providers/antigravity-quota.ts +3 -1
- package/src/providers/api-keys.ts +2 -1
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/derive.ts +5 -1
- package/src/providers/key-failover.ts +5 -1
- package/src/providers/openai-tiers.ts +5 -0
- package/src/providers/provider-id-rewrite.ts +1 -0
- package/src/providers/quota.ts +148 -50
- package/src/providers/registry.ts +27 -4
- package/src/providers/request-pacing.ts +33 -6
- package/src/providers/xai-transport.ts +21 -0
- package/src/responses/google-provider-options.ts +36 -0
- package/src/responses/namespace-tool-compat.ts +84 -4
- package/src/responses/parser.ts +11 -0
- package/src/responses/provider-opaque-metadata.ts +3 -3
- package/src/responses/schema.ts +37 -0
- package/src/responses/state.ts +94 -4
- package/src/router.ts +11 -2
- 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 +29 -0
- package/src/server/chat-completions.ts +2 -0
- package/src/server/images.ts +19 -35
- package/src/server/index.ts +94 -0
- package/src/server/management/agent-settings-routes.ts +205 -15
- package/src/server/management/combo-routes.ts +6 -0
- package/src/server/management/config-routes.ts +31 -5
- package/src/server/management/logs-usage-routes.ts +11 -5
- package/src/server/management/model-rows.ts +4 -0
- package/src/server/management/oauth-account-routes.ts +38 -7
- package/src/server/management/provider-routes.ts +113 -15
- package/src/server/management/routing-profile-routes.ts +3 -0
- package/src/server/port-reclaim.ts +19 -1
- package/src/server/request-log-conversation.ts +12 -0
- package/src/server/request-log.ts +23 -1
- package/src/server/responses/agent-task-recovery.ts +1 -1
- package/src/server/responses/compact.ts +30 -1
- package/src/server/responses/core.ts +363 -156
- package/src/server/responses/empty-completion-guard.ts +35 -6
- package/src/server/responses/fetch-helpers.ts +18 -5
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/v2-native-parent-override.ts +59 -0
- package/src/server/responses/ws-upstream.ts +75 -2
- package/src/server/responses-undeclared-tool-guard.ts +90 -8
- package/src/server/ws-bridge.ts +2 -1
- package/src/service.ts +1 -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/config.ts +16 -1
- package/src/types/provider.ts +18 -1
- package/src/types/request.ts +30 -0
- package/src/types/tools.ts +51 -0
- package/src/types.ts +6 -0
- 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/gemini-executor.ts +6 -4
- package/src/web-search/index.ts +16 -8
- package/src/web-search/loop.ts +42 -6
- package/gui/dist/assets/index-BG43zwVe.js +0 -102
- package/gui/dist/assets/index-CiSI-jrP.css +0 -1
package/src/adapters/base.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface IncomingMeta {
|
|
|
19
19
|
* anthropic adapter consumes it; others ignore it.
|
|
20
20
|
*/
|
|
21
21
|
imageTierBias?: number;
|
|
22
|
+
/** Provider-scoped structured error observation; never receives ordinary model payloads. */
|
|
23
|
+
onProviderError?: (error: { code?: string; status?: number; message?: string }) => void;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
export interface ProviderAdapter {
|
|
@@ -81,7 +83,7 @@ export interface AdapterRequest {
|
|
|
81
83
|
/** Client tool-search names actually lowered to upstream function calls for this request. */
|
|
82
84
|
convertedRoutedToolSearchNames?: ReadonlySet<string>;
|
|
83
85
|
/** Upstream-only aliases for namespace tools flattened in this request. */
|
|
84
|
-
convertedRoutedNamespaceToolAliases?: ReadonlyMap<string, { namespace: string; name: string }>;
|
|
86
|
+
convertedRoutedNamespaceToolAliases?: ReadonlyMap<string, { namespace: string; name: string; kind: "function" | "custom" }>;
|
|
85
87
|
/** Releases observation of a serialized request body after its final fetch attempt settles. */
|
|
86
88
|
releaseBodyObservation?: () => void;
|
|
87
89
|
/** Exact reasoning parameter emitted by the adapter, for request-log diagnostics only. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { execFile as execFileCallback } from "node:child_process";
|
|
3
3
|
import { promisify } from "node:util";
|
|
4
4
|
import { opendir } from "node:fs/promises";
|
|
@@ -12,7 +12,7 @@ import { commandCodeReasoningEfforts, refreshCommandCodeReasoningEfforts } from
|
|
|
12
12
|
import { identifyRoutedModel } from "./identity";
|
|
13
13
|
import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
|
|
14
14
|
import { parseDataUrl } from "./image";
|
|
15
|
-
import {
|
|
15
|
+
import { redactSecretString } from "../lib/redact";
|
|
16
16
|
|
|
17
17
|
// Retain the short ids emitted by the first local integration. New requests use the live catalog's
|
|
18
18
|
// provider-native IDs directly; this map is compatibility-only and is not a model fallback list.
|
|
@@ -27,6 +27,23 @@ function canonicalCommandCodeModelId(modelId: string): string {
|
|
|
27
27
|
return Object.hasOwn(COMMAND_CODE_MODEL_ALIASES, modelId) ? COMMAND_CODE_MODEL_ALIASES[modelId]! : modelId;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/** Surface Command Code's JSON error message to sidecar callers instead of a bare HTTP status. */
|
|
31
|
+
export function formatCommandCodeErrorBody(_status: number, _headers: Headers, payloadText: string): string {
|
|
32
|
+
try {
|
|
33
|
+
const payload = JSON.parse(payloadText) as unknown;
|
|
34
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return "";
|
|
35
|
+
const error = (payload as Record<string, unknown>).error;
|
|
36
|
+
const message = (error && typeof error === "object" && !Array.isArray(error)
|
|
37
|
+
? (error as Record<string, unknown>).message
|
|
38
|
+
: undefined) ?? (payload as Record<string, unknown>).message;
|
|
39
|
+
return typeof message === "string" && message.trim()
|
|
40
|
+
? redactSecretString(message.trim()).slice(0, 400)
|
|
41
|
+
: "";
|
|
42
|
+
} catch {
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
/** Flatten tool-result content for the text-only wire output, keeping an `[image]` marker per image part in content order. */
|
|
31
48
|
function toolResultText(content: string | OcxContentPart[]): string {
|
|
32
49
|
if (typeof content === "string") return content;
|
|
@@ -157,13 +174,17 @@ function visibleTools(parsed: OcxParsedRequest): OcxTool[] {
|
|
|
157
174
|
if (choice === "none") return [];
|
|
158
175
|
const tools = parsed.context.tools ?? [];
|
|
159
176
|
if (isAllowedToolChoice(choice)) {
|
|
160
|
-
return tools
|
|
177
|
+
return tools
|
|
178
|
+
.filter(toolChoiceToolPredicate(choice, tools))
|
|
179
|
+
.sort((left, right) => namespacedToolName(left.namespace, left.name).localeCompare(namespacedToolName(right.namespace, right.name)));
|
|
161
180
|
}
|
|
162
181
|
if (choice && typeof choice !== "string") {
|
|
163
182
|
const selected = resolveToolChoiceWireName(tools, choice.name);
|
|
164
|
-
return tools
|
|
183
|
+
return tools
|
|
184
|
+
.filter(tool => namespacedToolName(tool.namespace, tool.name) === selected)
|
|
185
|
+
.sort((left, right) => namespacedToolName(left.namespace, left.name).localeCompare(namespacedToolName(right.namespace, right.name)));
|
|
165
186
|
}
|
|
166
|
-
return tools;
|
|
187
|
+
return [...tools].sort((left, right) => namespacedToolName(left.namespace, left.name).localeCompare(namespacedToolName(right.namespace, right.name)));
|
|
167
188
|
}
|
|
168
189
|
|
|
169
190
|
function toolChoiceInstruction(parsed: OcxParsedRequest): string | undefined {
|
|
@@ -194,6 +215,9 @@ function currentWorkingDirectory(): string | undefined {
|
|
|
194
215
|
|
|
195
216
|
/** Cap the workspace listing so a large directory does not ship every entry name upstream. */
|
|
196
217
|
const MAX_WORKSPACE_STRUCTURE_ENTRIES = 64;
|
|
218
|
+
/** Cap directory entries scanned while selecting the stable workspace prefix. */
|
|
219
|
+
export const MAX_WORKSPACE_STRUCTURE_SCAN_ENTRIES = 4096;
|
|
220
|
+
// ponytail: the bounded scan trades complete directory coverage for request latency; raise only with measured need.
|
|
197
221
|
/** Cap how many recent commit subjects the config carries. */
|
|
198
222
|
const MAX_RECENT_COMMITS = 8;
|
|
199
223
|
/** Cap each recent commit entry to keep the request bounded even for long subjects. */
|
|
@@ -210,6 +234,46 @@ function projectSlug(cwd: string): string {
|
|
|
210
234
|
return cwd.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase().slice(0, 64) || "workspace";
|
|
211
235
|
}
|
|
212
236
|
|
|
237
|
+
function firstUserText(parsed: OcxParsedRequest): string | undefined {
|
|
238
|
+
for (const msg of parsed.context.messages) {
|
|
239
|
+
if (msg.role !== "user") continue;
|
|
240
|
+
if (typeof msg.content === "string") return msg.content;
|
|
241
|
+
const first = msg.content.find(part => part.type === "text" && typeof part.text === "string");
|
|
242
|
+
if (first && first.type === "text") return first.text;
|
|
243
|
+
}
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function commandCodeSessionId(parsed: OcxParsedRequest): string {
|
|
248
|
+
if (parsed._commandCodeSessionId) return parsed._commandCodeSessionId;
|
|
249
|
+
// Shared prompt-cache cohorts intentionally do not identify one conversation. Keep them out
|
|
250
|
+
// of upstream session affinity or unrelated conversations can pin to the same worker.
|
|
251
|
+
const threadId = parsed._clientThreadId?.trim();
|
|
252
|
+
const replayId = parsed._reasoningReplayScope?.clientThreadId?.trim();
|
|
253
|
+
const cursorId = parsed._cursorConversationId?.trim();
|
|
254
|
+
const cacheKey = !parsed._promptCacheKeyIsSharedCohort ? parsed.options.promptCacheKey?.trim() : undefined;
|
|
255
|
+
const rootText = firstUserText(parsed);
|
|
256
|
+
const identity = threadId
|
|
257
|
+
? ["thread", threadId]
|
|
258
|
+
: replayId
|
|
259
|
+
? ["replay", replayId]
|
|
260
|
+
: cursorId
|
|
261
|
+
? ["cursor", cursorId]
|
|
262
|
+
: cacheKey
|
|
263
|
+
? ["cache", cacheKey]
|
|
264
|
+
: rootText
|
|
265
|
+
? ["root", rootText]
|
|
266
|
+
: undefined;
|
|
267
|
+
const sessionId = !identity
|
|
268
|
+
? randomUUID()
|
|
269
|
+
: (() => {
|
|
270
|
+
const hex = createHash("sha256").update(`command-code:${identity[0]}\0${identity[1]}`).digest("hex");
|
|
271
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
|
|
272
|
+
})();
|
|
273
|
+
parsed._commandCodeSessionId = sessionId;
|
|
274
|
+
return sessionId;
|
|
275
|
+
}
|
|
276
|
+
|
|
213
277
|
interface GitWorkspaceInfo {
|
|
214
278
|
isGitRepo: boolean;
|
|
215
279
|
currentBranch: string;
|
|
@@ -219,6 +283,8 @@ interface GitWorkspaceInfo {
|
|
|
219
283
|
}
|
|
220
284
|
|
|
221
285
|
export const workspaceMetadataCache = new Map<string, { collectedAt: number; value: GitWorkspaceInfo }>();
|
|
286
|
+
export const workspaceConfigCache = new Map<string, { collectedAt: number; value: Record<string, unknown>; sessionId?: string }>();
|
|
287
|
+
export const SESSION_WORKSPACE_CONFIG_TTL_MS = 60 * 60_000;
|
|
222
288
|
|
|
223
289
|
/**
|
|
224
290
|
* Evict expired entries first, then the oldest live entry if at capacity.
|
|
@@ -245,6 +311,34 @@ export function pruneWorkspaceMetadataCache(now: number): void {
|
|
|
245
311
|
}
|
|
246
312
|
}
|
|
247
313
|
|
|
314
|
+
export function pruneWorkspaceConfigCache(now: number): void {
|
|
315
|
+
for (const [key, entry] of workspaceConfigCache) {
|
|
316
|
+
if (!entry.sessionId && now - entry.collectedAt >= WORKSPACE_METADATA_TTL_MS) {
|
|
317
|
+
workspaceConfigCache.delete(key);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (workspaceConfigCache.size >= MAX_WORKSPACE_METADATA_ENTRIES) {
|
|
321
|
+
let oldestKey: string | null = null;
|
|
322
|
+
let oldestAt = Infinity;
|
|
323
|
+
for (const [key, entry] of workspaceConfigCache) {
|
|
324
|
+
if (entry.sessionId) continue;
|
|
325
|
+
if (entry.collectedAt < oldestAt) {
|
|
326
|
+
oldestAt = entry.collectedAt;
|
|
327
|
+
oldestKey = key;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (oldestKey === null) {
|
|
331
|
+
for (const [key, entry] of workspaceConfigCache) {
|
|
332
|
+
if (entry.collectedAt < oldestAt) {
|
|
333
|
+
oldestAt = entry.collectedAt;
|
|
334
|
+
oldestKey = key;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (oldestKey !== null) workspaceConfigCache.delete(oldestKey);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
248
342
|
const execFile = promisify(execFileCallback);
|
|
249
343
|
|
|
250
344
|
/** Best-effort git metadata for the upstream config contract; every read fails safe and stays off the event loop. */
|
|
@@ -281,32 +375,53 @@ async function gitWorkspaceInfo(cwd: string | undefined): Promise<GitWorkspaceIn
|
|
|
281
375
|
return value;
|
|
282
376
|
}
|
|
283
377
|
|
|
284
|
-
async function commandCodeConfig(cwd: string | undefined): Promise<Record<string, unknown>> {
|
|
378
|
+
export async function commandCodeConfig(cwd: string | undefined, sessionId?: string): Promise<Record<string, unknown>> {
|
|
379
|
+
const cacheKey = sessionId ? `${sessionId}:${cwd ?? ""}` : (cwd ?? "");
|
|
380
|
+
const now = Date.now();
|
|
381
|
+
const cached = cacheKey ? workspaceConfigCache.get(cacheKey) : undefined;
|
|
382
|
+
if (cacheKey) {
|
|
383
|
+
if (cached && (sessionId || now - cached.collectedAt < WORKSPACE_METADATA_TTL_MS)) return cached.value;
|
|
384
|
+
}
|
|
285
385
|
let structure: string[] = [];
|
|
286
386
|
if (cwd) {
|
|
287
387
|
try {
|
|
288
|
-
//
|
|
289
|
-
//
|
|
388
|
+
// Keep only the lexicographically smallest entries within the bounded scan so filesystem
|
|
389
|
+
// enumeration order cannot change the selected prefix for the scanned portion.
|
|
290
390
|
const dir = await opendir(cwd);
|
|
291
391
|
try {
|
|
292
|
-
|
|
392
|
+
const entries = dir[Symbol.asyncIterator]();
|
|
393
|
+
for (let scanned = 0; scanned < MAX_WORKSPACE_STRUCTURE_SCAN_ENTRIES; scanned += 1) {
|
|
394
|
+
const next = await entries.next();
|
|
395
|
+
if (next.done) break;
|
|
396
|
+
const entry = next.value;
|
|
293
397
|
if (entry.name.startsWith(".")) continue;
|
|
294
398
|
structure.push(entry.name);
|
|
295
|
-
if (structure.length
|
|
399
|
+
if (structure.length > MAX_WORKSPACE_STRUCTURE_ENTRIES) {
|
|
400
|
+
structure.sort();
|
|
401
|
+
structure.pop();
|
|
402
|
+
}
|
|
296
403
|
}
|
|
297
404
|
} finally {
|
|
298
405
|
await dir.close().catch(() => undefined);
|
|
299
406
|
}
|
|
300
407
|
} catch { /* workspace metadata is optional */ }
|
|
301
408
|
}
|
|
409
|
+
structure.sort();
|
|
302
410
|
const git = await gitWorkspaceInfo(cwd);
|
|
303
|
-
|
|
411
|
+
const value = {
|
|
304
412
|
...(cwd ? { workingDir: cwd } : {}),
|
|
305
|
-
date:
|
|
413
|
+
date: sessionId && typeof cached?.value.date === "string"
|
|
414
|
+
? cached.value.date
|
|
415
|
+
: new Date(now).toISOString().slice(0, 10),
|
|
306
416
|
environment: process.platform,
|
|
307
417
|
structure,
|
|
308
418
|
...git,
|
|
309
419
|
};
|
|
420
|
+
if (cacheKey) {
|
|
421
|
+
if (!workspaceConfigCache.has(cacheKey)) pruneWorkspaceConfigCache(now);
|
|
422
|
+
workspaceConfigCache.set(cacheKey, { collectedAt: now, value, ...(sessionId ? { sessionId } : {}) });
|
|
423
|
+
}
|
|
424
|
+
return value;
|
|
310
425
|
}
|
|
311
426
|
|
|
312
427
|
function usage(value: unknown): OcxUsage | undefined {
|
|
@@ -451,9 +566,11 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA
|
|
|
451
566
|
const executor = (provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? globalThis.fetch;
|
|
452
567
|
return {
|
|
453
568
|
name: "command-code",
|
|
569
|
+
formatErrorBody: formatCommandCodeErrorBody,
|
|
454
570
|
async buildRequest(parsed: OcxParsedRequest): Promise<AdapterRequest> {
|
|
455
571
|
if (!provider.apiKey) throw new Error("Command Code credential missing — run ocx login command-code");
|
|
456
572
|
const cwd = currentWorkingDirectory();
|
|
573
|
+
const sessionId = commandCodeSessionId(parsed);
|
|
457
574
|
const tools = visibleTools(parsed);
|
|
458
575
|
const toolNudge = buildNonOpenAIToolCatalogNudgeForTools(tools, parsed.options.toolChoice);
|
|
459
576
|
const choiceInstruction = toolChoiceInstruction(parsed);
|
|
@@ -463,11 +580,8 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA
|
|
|
463
580
|
...(choiceInstruction ? [choiceInstruction] : []),
|
|
464
581
|
].join("\n\n"), parsed.modelId);
|
|
465
582
|
const reasoningEffort = supportedCommandCodeEffort(provider, parsed.modelId, parsed.options.reasoning);
|
|
466
|
-
const projectContext = provider.projectContext === "on"
|
|
467
|
-
? await loadCommandCodeProjectContext(cwd)
|
|
468
|
-
: EMPTY_COMMAND_CODE_PROJECT_CONTEXT;
|
|
469
583
|
const body = {
|
|
470
|
-
config: await commandCodeConfig(cwd),
|
|
584
|
+
config: await commandCodeConfig(cwd, sessionId), memory: "", taste: null, skills: null,
|
|
471
585
|
permissionMode: "standard", mode: "agent",
|
|
472
586
|
params: {
|
|
473
587
|
model: canonicalCommandCodeModelId(parsed.modelId),
|
|
@@ -490,7 +604,7 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA
|
|
|
490
604
|
"x-cli-environment": "production",
|
|
491
605
|
"x-taste-learning": "false",
|
|
492
606
|
"x-co-flag": "false",
|
|
493
|
-
"x-session-id":
|
|
607
|
+
"x-session-id": sessionId,
|
|
494
608
|
};
|
|
495
609
|
if (cwd) headers["x-project-slug"] = projectSlug(cwd);
|
|
496
610
|
return {
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External Cursor output quarantine (devlog 260826 gaps 10-11).
|
|
3
|
+
*
|
|
4
|
+
* Gap 10: flattened tool-result history can prime an external model to echo the
|
|
5
|
+
* "[Tool Result]" envelope as its own reply.
|
|
6
|
+
*
|
|
7
|
+
* Gap 11: a model can invent a blocked native-tool attempt in visible commentary
|
|
8
|
+
* even though the only real current-turn tool is the advertised Codex bridge.
|
|
9
|
+
*
|
|
10
|
+
* Both failures are observable only at the output boundary. The sniffers below
|
|
11
|
+
* hold a bounded prefix until it either diverges or proves the failure, allowing
|
|
12
|
+
* cursor.ts to retry before any invalid text reaches the client.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const ECHO_MARKERS = ["[Tool Result]", "[Tool Error]", "[tool_result]"] as const;
|
|
16
|
+
const MAX_SNIFF_BYTES = 40;
|
|
17
|
+
const MAX_ROUTING_COMMENTARY_BYTES = 512;
|
|
18
|
+
/** Aggregate quarantine cap: past this, flush and disarm. */
|
|
19
|
+
const MAX_HOLD_BYTES = 8 * 1024;
|
|
20
|
+
const encoder = new TextEncoder();
|
|
21
|
+
|
|
22
|
+
export class CursorToolResultEchoError extends Error {
|
|
23
|
+
readonly code = "cursor_tool_result_echo";
|
|
24
|
+
constructor(marker: string) {
|
|
25
|
+
super(
|
|
26
|
+
"Cursor external model echoed the replayed tool-result envelope (\"" + marker
|
|
27
|
+
+ "\") instead of continuing the task.",
|
|
28
|
+
);
|
|
29
|
+
this.name = "CursorToolResultEchoError";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class CursorRoutingCommentaryError extends Error {
|
|
34
|
+
readonly code = "cursor_routing_commentary_hallucination";
|
|
35
|
+
constructor() {
|
|
36
|
+
super("Cursor external model invented a blocked tool surface before any tool call occurred.");
|
|
37
|
+
this.name = "CursorRoutingCommentaryError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type EchoSnifferDecision =
|
|
42
|
+
| { kind: "hold" }
|
|
43
|
+
| { kind: "flush" }
|
|
44
|
+
| { kind: "echo"; marker: string };
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Incremental envelope-prefix sniffer. Leading whitespace is tolerated so a
|
|
48
|
+
* marker copied after a newline is still caught.
|
|
49
|
+
*/
|
|
50
|
+
export class CursorEnvelopeEchoSniffer {
|
|
51
|
+
private buffered = "";
|
|
52
|
+
private byteCount = 0;
|
|
53
|
+
private done = false;
|
|
54
|
+
|
|
55
|
+
get settled(): boolean {
|
|
56
|
+
return this.done;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
feed(textDelta: string): EchoSnifferDecision {
|
|
60
|
+
if (this.done) return { kind: "flush" };
|
|
61
|
+
this.buffered += textDelta;
|
|
62
|
+
this.byteCount += encoder.encode(textDelta).byteLength;
|
|
63
|
+
const probe = this.buffered.replace(/^\s+/, "");
|
|
64
|
+
for (const marker of ECHO_MARKERS) {
|
|
65
|
+
if (probe.startsWith(marker)) {
|
|
66
|
+
this.done = true;
|
|
67
|
+
return { kind: "echo", marker };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const stillPrefix = ECHO_MARKERS.some(marker =>
|
|
71
|
+
probe.length < marker.length && marker.startsWith(probe),
|
|
72
|
+
);
|
|
73
|
+
if (stillPrefix && this.byteCount <= MAX_SNIFF_BYTES && this.buffered.length < MAX_HOLD_BYTES) {
|
|
74
|
+
return { kind: "hold" };
|
|
75
|
+
}
|
|
76
|
+
this.done = true;
|
|
77
|
+
return { kind: "flush" };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
finish(): EchoSnifferDecision {
|
|
81
|
+
if (this.done) return { kind: "flush" };
|
|
82
|
+
this.done = true;
|
|
83
|
+
return { kind: "flush" };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type RoutingCommentaryDecision =
|
|
88
|
+
| { kind: "hold" }
|
|
89
|
+
| { kind: "flush" }
|
|
90
|
+
| { kind: "hallucination" };
|
|
91
|
+
|
|
92
|
+
const ROUTING_NATIVE_TOOL_NAME = /\b(shell|read|grep|list|bash|filesystem)\b/giu;
|
|
93
|
+
const ROUTING_TOOL_HINT =
|
|
94
|
+
/(?:\b(?:shell|read|grep|list|bash|filesystem)\b|exec_command|shell_command|브리지|네이티브\s*(?:셸|쉘))/iu;
|
|
95
|
+
const ROUTING_FAILURE_OR_BRIDGE_CLAIM =
|
|
96
|
+
/(?:blocked|unavailable|interrupted|bridged|bridge|차단|중단|막혀)/iu;
|
|
97
|
+
const ROUTING_REDIRECT_CLAIM =
|
|
98
|
+
/(?:exec_command|shell_command|\bexec\b|브리지|bridged|bridge|redirected|fallback|switch(?:ed|ing)?|host shell|전환|우회|통과(?:되|하)|다른\s*(?:도구|경로)|경로로)/iu;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Quarantines the first line of code-mode / bridge output long enough to reject
|
|
102
|
+
* an impossible routing claim. It requires a failure or bridge claim plus either an
|
|
103
|
+
* explicit redirect to another execution surface or two distinct unadvertised
|
|
104
|
+
* native-tool names; a legitimate sentence such as "Shell is unavailable on
|
|
105
|
+
* this OS" therefore passes.
|
|
106
|
+
*/
|
|
107
|
+
export class CursorRoutingCommentarySniffer {
|
|
108
|
+
private buffered = "";
|
|
109
|
+
private byteCount = 0;
|
|
110
|
+
private done = false;
|
|
111
|
+
|
|
112
|
+
get settled(): boolean {
|
|
113
|
+
return this.done;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
feed(textDelta: string): RoutingCommentaryDecision {
|
|
117
|
+
if (this.done) return { kind: "flush" };
|
|
118
|
+
this.buffered += textDelta;
|
|
119
|
+
this.byteCount += encoder.encode(textDelta).byteLength;
|
|
120
|
+
if (this.matchesHallucination()) {
|
|
121
|
+
this.done = true;
|
|
122
|
+
return { kind: "hallucination" };
|
|
123
|
+
}
|
|
124
|
+
const lineBreakCount = (this.buffered.match(/\n/gu) ?? []).length;
|
|
125
|
+
const hasRoutingHint = ROUTING_TOOL_HINT.test(this.buffered) || ROUTING_FAILURE_OR_BRIDGE_CLAIM.test(this.buffered);
|
|
126
|
+
const pendingFailureClaim =
|
|
127
|
+
ROUTING_TOOL_HINT.test(this.buffered)
|
|
128
|
+
&& ROUTING_FAILURE_OR_BRIDGE_CLAIM.test(this.buffered)
|
|
129
|
+
&& lineBreakCount < 2;
|
|
130
|
+
if (
|
|
131
|
+
this.byteCount < MAX_ROUTING_COMMENTARY_BYTES
|
|
132
|
+
&& this.buffered.length < MAX_HOLD_BYTES
|
|
133
|
+
&& (lineBreakCount === 0 || pendingFailureClaim)
|
|
134
|
+
&& (hasRoutingHint || this.byteCount < 64)
|
|
135
|
+
) {
|
|
136
|
+
return { kind: "hold" };
|
|
137
|
+
}
|
|
138
|
+
this.done = true;
|
|
139
|
+
return { kind: "flush" };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
finish(): RoutingCommentaryDecision {
|
|
143
|
+
if (this.done) return { kind: "flush" };
|
|
144
|
+
this.done = true;
|
|
145
|
+
return this.matchesHallucination() ? { kind: "hallucination" } : { kind: "flush" };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private matchesHallucination(): boolean {
|
|
149
|
+
if (!ROUTING_FAILURE_OR_BRIDGE_CLAIM.test(this.buffered)) return false;
|
|
150
|
+
const nativeTools = new Set(
|
|
151
|
+
[...this.buffered.matchAll(ROUTING_NATIVE_TOOL_NAME)].map(match => match[1]?.toLowerCase()),
|
|
152
|
+
);
|
|
153
|
+
if (nativeTools.size === 0) return false;
|
|
154
|
+
return ROUTING_REDIRECT_CLAIM.test(this.buffered) || nativeTools.size >= 2;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export const CURSOR_ECHO_RETRY_CONTINUATION_TEXT =
|
|
159
|
+
"Your previous reply copied an internal tool-output record verbatim and was rejected. Continue the original task now: issue the next required tool call, or answer in your own words if no tool is needed.";
|
|
160
|
+
|
|
161
|
+
export const CURSOR_ROUTING_COMMENTARY_RETRY_TEXT =
|
|
162
|
+
"Your previous reply claimed an execution-surface failure that did not occur and was rejected. Use the current tool catalog as ground truth. Perform the requested operation through the advertised execution tool now, with no commentary before the tool call.";
|
|
@@ -75,6 +75,7 @@ import { desktopDepsFromConfig } from "./native-exec-desktop";
|
|
|
75
75
|
import {
|
|
76
76
|
buildCursorToolDefinitions,
|
|
77
77
|
cursorRequestAdvertisesApplyPatch,
|
|
78
|
+
cursorRequestUsesCodeMode,
|
|
78
79
|
cursorRequestHasShellAlias,
|
|
79
80
|
cursorToolArgNormalizeSchema,
|
|
80
81
|
cursorToolWireName,
|
|
@@ -108,7 +109,7 @@ const CURSOR_STREAM_SILENCE_FAIL_MS = 30_000;
|
|
|
108
109
|
* for this long is equally stuck — the server is alive but the turn is not progressing.
|
|
109
110
|
* Reset on every decoded frame that is not liveness-only.
|
|
110
111
|
*/
|
|
111
|
-
const CURSOR_STREAM_HEARTBEAT_ONLY_FAIL_MS =
|
|
112
|
+
const CURSOR_STREAM_HEARTBEAT_ONLY_FAIL_MS = 180_000;
|
|
112
113
|
/**
|
|
113
114
|
* After `turnEnded` is decoded, the application turn is complete. A server that keeps
|
|
114
115
|
* HTTP/2 open past this point cannot hold the turn hostage (senpi #1062): we close our side
|
|
@@ -646,6 +647,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
646
647
|
this.execContext = {
|
|
647
648
|
...this.execContext,
|
|
648
649
|
clientToolDefs,
|
|
650
|
+
codeMode: cursorRequestUsesCodeMode(request.tools, request.toolChoice),
|
|
649
651
|
rejectNativeFileMutations: cursorRequestAdvertisesApplyPatch(request.tools, request.toolChoice),
|
|
650
652
|
structuredEditAvailable: syntheticStructuredEditToolNames.size > 0,
|
|
651
653
|
};
|
|
@@ -1509,7 +1511,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
1509
1511
|
const awaitedNativeArgsBeforeMapping = update?.case === "toolCallCompleted"
|
|
1510
1512
|
&& state.openToolCalls.get(update.value.callId)?.awaitingNativeArgs === true;
|
|
1511
1513
|
const mapped = mapCursorProtobufServerMessage(message, state);
|
|
1512
|
-
if (mapped.some(event => event.type === "text")) this.sawAssistantText = true;
|
|
1514
|
+
if (mapped.some(event => event.type === "text" || event.type === "thinking" || event.type === "tool_call_start" || event.type === "tool_call_end")) this.sawAssistantText = true;
|
|
1513
1515
|
const beganAwaitingNativeClientToolArgs = update?.case === "toolCallCompleted"
|
|
1514
1516
|
&& !awaitedNativeArgsBeforeMapping
|
|
1515
1517
|
&& state.openToolCalls.get(update.value.callId)?.awaitingNativeArgs === true;
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
type GrepArgs,
|
|
35
35
|
} from "./gen/agent_pb";
|
|
36
36
|
import { errorText, execBytes, lineCount, textDecoder, textEncoder } from "./native-exec-common";
|
|
37
|
+
import { nativeLocalExecDisabledMessage, type CursorNativeExecPolicyContext } from "./native-exec-policy";
|
|
37
38
|
|
|
38
39
|
const MAX_GREP_FILES = 500;
|
|
39
40
|
const MAX_GREP_RESULTS = 200;
|
|
@@ -46,14 +47,14 @@ function codexNativeMutationRefusal(operation: "write" | "delete", structuredEdi
|
|
|
46
47
|
return `Cursor-native ${operation} is disabled for this Codex request because apply_patch is available.${structuredHint} No file was changed.`;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
const NATIVE_LOCAL_EXEC_DISABLED =
|
|
50
|
-
"
|
|
50
|
+
export const NATIVE_LOCAL_EXEC_DISABLED =
|
|
51
|
+
"Re-issue this operation NOW through the catalog shell tool (`shell_command` / `exec_command`, or the listed `mcp_opencodex-responses_*` display alias) with the host-shell-safe equivalent: POSIX (`cat`, `head`, `ls`, `rg`, `grep`) or Windows PowerShell (`Get-Content`, `Get-ChildItem`, `Select-String`); use `apply_patch` for file edits. Do NOT narrate this redirect, do NOT comment on tool availability, and do NOT re-announce the task — just make the bridge call.";
|
|
51
52
|
|
|
52
|
-
export function rejectReadExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
|
|
53
|
+
export function rejectReadExecForPolicy(execMsg: ExecServerMessage, opts: CursorNativeExecPolicyContext = {}): Uint8Array {
|
|
53
54
|
if (execMsg.message.case !== "readArgs") throw new Error("invalid read exec");
|
|
54
55
|
const path = resolve(execMsg.message.value.path);
|
|
55
56
|
return execBytes(execMsg, "readResult", create(ReadResultSchema, {
|
|
56
|
-
result: { case: "error", value: create(ReadErrorSchema, { path, error:
|
|
57
|
+
result: { case: "error", value: create(ReadErrorSchema, { path, error: nativeLocalExecDisabledMessage(opts) }) },
|
|
57
58
|
}));
|
|
58
59
|
}
|
|
59
60
|
|
|
@@ -98,13 +99,13 @@ export function rejectWriteExecForApplyPatch(execMsg: ExecServerMessage, structu
|
|
|
98
99
|
}));
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
export function rejectWriteExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
|
|
102
|
+
export function rejectWriteExecForPolicy(execMsg: ExecServerMessage, opts: CursorNativeExecPolicyContext = {}): Uint8Array {
|
|
102
103
|
if (execMsg.message.case !== "writeArgs") throw new Error("invalid write exec");
|
|
103
104
|
const path = resolve(execMsg.message.value.path);
|
|
104
105
|
return execBytes(execMsg, "writeResult", create(WriteResultSchema, {
|
|
105
106
|
result: {
|
|
106
107
|
case: "rejected",
|
|
107
|
-
value: create(WriteRejectedSchema, { path, reason: `${
|
|
108
|
+
value: create(WriteRejectedSchema, { path, reason: `${nativeLocalExecDisabledMessage(opts)} No file was changed.` }),
|
|
108
109
|
},
|
|
109
110
|
}));
|
|
110
111
|
}
|
|
@@ -147,13 +148,13 @@ export function rejectDeleteExecForApplyPatch(execMsg: ExecServerMessage, struct
|
|
|
147
148
|
}));
|
|
148
149
|
}
|
|
149
150
|
|
|
150
|
-
export function rejectDeleteExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
|
|
151
|
+
export function rejectDeleteExecForPolicy(execMsg: ExecServerMessage, opts: CursorNativeExecPolicyContext = {}): Uint8Array {
|
|
151
152
|
if (execMsg.message.case !== "deleteArgs") throw new Error("invalid delete exec");
|
|
152
153
|
const path = resolve(execMsg.message.value.path);
|
|
153
154
|
return execBytes(execMsg, "deleteResult", create(DeleteResultSchema, {
|
|
154
155
|
result: {
|
|
155
156
|
case: "rejected",
|
|
156
|
-
value: create(DeleteRejectedSchema, { path, reason: `${
|
|
157
|
+
value: create(DeleteRejectedSchema, { path, reason: `${nativeLocalExecDisabledMessage(opts)} No file was changed.` }),
|
|
157
158
|
},
|
|
158
159
|
}));
|
|
159
160
|
}
|
|
@@ -188,11 +189,11 @@ export function deleteExec(execMsg: ExecServerMessage): Uint8Array {
|
|
|
188
189
|
}
|
|
189
190
|
}
|
|
190
191
|
|
|
191
|
-
export function rejectLsExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
|
|
192
|
+
export function rejectLsExecForPolicy(execMsg: ExecServerMessage, opts: CursorNativeExecPolicyContext = {}): Uint8Array {
|
|
192
193
|
if (execMsg.message.case !== "lsArgs") throw new Error("invalid ls exec");
|
|
193
194
|
const path = resolve(execMsg.message.value.path);
|
|
194
195
|
return execBytes(execMsg, "lsResult", create(LsResultSchema, {
|
|
195
|
-
result: { case: "error", value: create(LsErrorSchema, { path, error:
|
|
196
|
+
result: { case: "error", value: create(LsErrorSchema, { path, error: nativeLocalExecDisabledMessage(opts) }) },
|
|
196
197
|
}));
|
|
197
198
|
}
|
|
198
199
|
|
|
@@ -256,8 +257,8 @@ function grepError(execMsg: ExecServerMessage, error: string): Uint8Array {
|
|
|
256
257
|
}));
|
|
257
258
|
}
|
|
258
259
|
|
|
259
|
-
export function rejectGrepExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
|
|
260
|
-
return grepError(execMsg,
|
|
260
|
+
export function rejectGrepExecForPolicy(execMsg: ExecServerMessage, opts: CursorNativeExecPolicyContext = {}): Uint8Array {
|
|
261
|
+
return grepError(execMsg, nativeLocalExecDisabledMessage(opts));
|
|
261
262
|
}
|
|
262
263
|
|
|
263
264
|
export function grepExec(execMsg: ExecServerMessage): Uint8Array {
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import { create } from "@bufbuild/protobuf";
|
|
2
2
|
import { FetchErrorSchema, FetchResultSchema, FetchSuccessSchema, type ExecServerMessage } from "./gen/agent_pb";
|
|
3
3
|
import { errorText, execBytes } from "./native-exec-common";
|
|
4
|
+
import { nativeFetchDisabledMessage, type CursorNativeExecPolicyContext } from "./native-exec-policy";
|
|
4
5
|
|
|
5
6
|
export interface CursorNativeNetworkDeps {
|
|
6
7
|
fetch?: typeof fetch;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
"Cursor-native fetch is not executed locally. Use the Codex shell bridge tool `shell_command` (aliases: `exec_command`, `mcp_opencodex-responses_shell_command`, `mcp_opencodex-responses_exec_command`) with curl or wget.";
|
|
11
|
-
|
|
12
|
-
export function rejectFetchExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
|
|
10
|
+
export function rejectFetchExecForPolicy(execMsg: ExecServerMessage, opts: CursorNativeExecPolicyContext = {}): Uint8Array {
|
|
13
11
|
if (execMsg.message.case !== "fetchArgs") throw new Error("invalid fetch exec");
|
|
14
12
|
const args = execMsg.message.value;
|
|
15
13
|
return execBytes(execMsg, "fetchResult", create(FetchResultSchema, {
|
|
16
|
-
result: { case: "error", value: create(FetchErrorSchema, { url: args.url, error:
|
|
14
|
+
result: { case: "error", value: create(FetchErrorSchema, { url: args.url, error: nativeFetchDisabledMessage(opts) }) },
|
|
17
15
|
}));
|
|
18
16
|
}
|
|
19
17
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface CursorNativeExecPolicyContext {
|
|
2
|
+
codeMode?: boolean;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function nativeLocalExecDisabledMessage(opts: CursorNativeExecPolicyContext = {}): string {
|
|
6
|
+
if (opts.codeMode) {
|
|
7
|
+
return (
|
|
8
|
+
"Cursor-native local tools are policy-redirected to the Codex bridge for this request; this is not a permissions denial. "
|
|
9
|
+
+ "Use the top-level `exec` tool and call nested helpers inside its JavaScript body as `await tools.<name>(args)` "
|
|
10
|
+
+ "(for example `await tools.exec_command({cmd: \"ls\"})`, `await tools.apply_patch(input)`, or another helper listed in `ALL_TOOLS`). "
|
|
11
|
+
+ "Do not call `shell_command` or `exec_command` at the top level in code mode."
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
return (
|
|
15
|
+
"Re-issue this operation NOW through the catalog shell tool (`shell_command` / `exec_command`, or the listed `mcp_opencodex-responses_*` display alias) with the host-shell-safe equivalent: POSIX (`cat`, `head`, `ls`, `rg`, `grep`) or Windows PowerShell (`Get-Content`, `Get-ChildItem`, `Select-String`); use `apply_patch` for file edits. Do NOT narrate this redirect, do NOT comment on tool availability, and do NOT re-announce the task — just make the bridge call."
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function nativeFetchDisabledMessage(opts: CursorNativeExecPolicyContext = {}): string {
|
|
20
|
+
if (opts.codeMode) {
|
|
21
|
+
return (
|
|
22
|
+
"Cursor-native fetch is policy-redirected to the Codex bridge for this request; this is not a permissions denial. "
|
|
23
|
+
+ "Use the top-level `exec` tool and call a nested shell helper inside its JavaScript body as `await tools.exec_command({cmd: \"curl ...\"})` "
|
|
24
|
+
+ "(or another helper listed in `ALL_TOOLS`). Do not call `shell_command` or `exec_command` at the top level in code mode."
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return (
|
|
28
|
+
"Re-issue this fetch NOW through the catalog shell tool `shell_command` (aliases: `exec_command`, `mcp_opencodex-responses_shell_command`, `mcp_opencodex-responses_exec_command`) with curl or wget. Do NOT narrate this redirect or comment on tool availability — just make the bridge call."
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function nativeShellDisabledMessage(opts: CursorNativeExecPolicyContext = {}): string {
|
|
33
|
+
if (opts.codeMode) {
|
|
34
|
+
return (
|
|
35
|
+
"Cursor-native shell is policy-redirected to the Codex bridge for this request; this is not a permissions denial. "
|
|
36
|
+
+ "Use the top-level `exec` tool and call a nested shell helper inside its JavaScript body as `await tools.exec_command({cmd: \"...\"})` "
|
|
37
|
+
+ "(or another helper listed in `ALL_TOOLS`). Do not call `shell_command` or `exec_command` at the top level in code mode."
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return (
|
|
41
|
+
"Re-issue this command NOW through the catalog shell tool (`shell_command` or `exec_command`; the long `mcp_opencodex-responses_*` display name is the same tool). "
|
|
42
|
+
+ "Adapt the command for the Codex client host shell before calling the bridge "
|
|
43
|
+
+ "(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 `&&`). "
|
|
44
|
+
+ "Make at most one corrected bridge attempt after a failure, then report the error and stop — do not repeat equivalent failing commands. "
|
|
45
|
+
+ "Do NOT narrate this redirect, do NOT comment on tool availability, and do NOT re-announce the task — just make the bridge call."
|
|
46
|
+
);
|
|
47
|
+
}
|