@yansigit/opencodex 2.31.3 → 2.33.0

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.
Files changed (176) hide show
  1. package/README.md +2 -2
  2. package/bin/ocx.mjs +99 -70
  3. package/gui/dist/assets/index-DKLr4LTE.js +102 -0
  4. package/gui/dist/assets/index-DrSQdTRd.css +1 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +6 -5
  7. package/src/adapters/anthropic.ts +25 -13
  8. package/src/adapters/azure.ts +20 -4
  9. package/src/adapters/base.ts +5 -1
  10. package/src/adapters/command-code.ts +42 -10
  11. package/src/adapters/cursor/live-models.ts +8 -0
  12. package/src/adapters/cursor/live-transport.ts +1 -1
  13. package/src/adapters/cursor/native-exec-desktop.ts +16 -0
  14. package/src/adapters/cursor/protobuf-events.ts +158 -7
  15. package/src/adapters/cursor/protobuf-request.ts +33 -15
  16. package/src/adapters/cursor/request-builder.ts +4 -3
  17. package/src/adapters/cursor/tool-definitions.ts +27 -1
  18. package/src/adapters/cursor/types.ts +4 -3
  19. package/src/adapters/cursor.ts +9 -0
  20. package/src/adapters/google-antigravity-replay.ts +2 -2
  21. package/src/adapters/google-antigravity-wire.ts +7 -0
  22. package/src/adapters/google-errors.ts +6 -2
  23. package/src/adapters/google-http.ts +30 -7
  24. package/src/adapters/google-truncation.ts +5 -0
  25. package/src/adapters/google-wire-compiler.ts +38 -6
  26. package/src/adapters/google.ts +154 -33
  27. package/src/adapters/kiro-tools.ts +20 -9
  28. package/src/adapters/kiro.ts +0 -3
  29. package/src/adapters/openai-chat.ts +9 -0
  30. package/src/adapters/openai-responses.ts +4 -1
  31. package/src/adapters/tool-catalog-nudge.ts +1 -1
  32. package/src/adapters/xai-web-search.ts +7 -2
  33. package/src/bridge.ts +133 -24
  34. package/src/claude/context-windows.ts +16 -9
  35. package/src/cli/dispatch.ts +50 -2
  36. package/src/cli/doctor.ts +26 -13
  37. package/src/cli/help.ts +4 -3
  38. package/src/cli/index.ts +20 -6
  39. package/src/cli/models.ts +13 -3
  40. package/src/cli/observe.ts +20 -5
  41. package/src/cli/provider.ts +8 -1
  42. package/src/cli/registry.ts +7 -5
  43. package/src/cli/status.ts +25 -1
  44. package/src/cli/system-restart-client.ts +1 -1
  45. package/src/cli/usage-report.ts +134 -0
  46. package/src/codex/app-server-processes.ts +3 -1
  47. package/src/codex/auth-api.ts +4 -2
  48. package/src/codex/autostart-health.ts +16 -0
  49. package/src/codex/catalog/aggregation.ts +12 -0
  50. package/src/codex/catalog/effort.ts +42 -12
  51. package/src/codex/catalog/metadata.ts +27 -1
  52. package/src/codex/catalog/model-metadata.ts +593 -0
  53. package/src/codex/catalog/parsing.ts +71 -27
  54. package/src/codex/catalog/provider-fetch.ts +189 -33
  55. package/src/codex/catalog/sync.ts +6 -5
  56. package/src/codex/convergence.ts +5 -0
  57. package/src/codex/desktop-app-restart.ts +342 -0
  58. package/src/codex/history-job.ts +32 -3
  59. package/src/codex/history-manifest.ts +112 -0
  60. package/src/codex/history-migration-guardian.ts +5 -5
  61. package/src/codex/history-provider.ts +825 -247
  62. package/src/codex/history-worker.ts +8 -5
  63. package/src/codex/inject.ts +49 -21
  64. package/src/codex/injected-marker.ts +1 -1
  65. package/src/codex/internal/history-writer.ts +4 -3
  66. package/src/codex/native-profile-startup.ts +157 -27
  67. package/src/codex/native-residue.ts +26 -33
  68. package/src/codex/shim.ts +56 -3
  69. package/src/combos/failover.ts +27 -0
  70. package/src/compatibility/index.ts +26 -0
  71. package/src/compatibility/manifest.ts +253 -0
  72. package/src/compatibility/openai-responses.ts +81 -0
  73. package/src/config/atomic-write.ts +219 -0
  74. package/src/config/paths.ts +40 -0
  75. package/src/config/process-state.ts +308 -0
  76. package/src/config/provider-validation.ts +214 -0
  77. package/src/config.ts +153 -814
  78. package/src/generated/compatibility-version.json +232 -148
  79. package/src/images/loop.ts +37 -6
  80. package/src/images/plan.ts +5 -4
  81. package/src/integrations/ownership-policy.ts +141 -0
  82. package/src/integrations/ownership.ts +10 -0
  83. package/src/integrations/state.ts +44 -5
  84. package/src/integrations/writer.ts +6 -0
  85. package/src/lib/azure-identity.ts +154 -0
  86. package/src/lib/bounded-body.ts +14 -2
  87. package/src/lib/debug.ts +42 -0
  88. package/src/lib/errors.ts +14 -0
  89. package/src/lib/process-control.ts +2 -1
  90. package/src/lib/provider-outbound.ts +45 -33
  91. package/src/lib/provider-tls-profile.ts +309 -0
  92. package/src/lib/proxy-env.ts +49 -0
  93. package/src/lib/redact.ts +10 -1
  94. package/src/lib/state-store-registrations.ts +2 -0
  95. package/src/lib/tool-argument-integers.ts +56 -5
  96. package/src/oauth/antigravity-routing.ts +282 -236
  97. package/src/oauth/callback-server.ts +22 -2
  98. package/src/oauth/command-code.ts +5 -16
  99. package/src/oauth/google-antigravity.ts +42 -5
  100. package/src/oauth/health.ts +1 -1
  101. package/src/oauth/index.ts +15 -3
  102. package/src/oauth/kimi.ts +9 -1
  103. package/src/oauth/open-browser-choice.ts +26 -0
  104. package/src/oauth/store.ts +6 -0
  105. package/src/providers/antigravity-quota.ts +3 -1
  106. package/src/providers/api-keys.ts +2 -1
  107. package/src/providers/auto-compact-budget.ts +65 -0
  108. package/src/providers/derive.ts +4 -0
  109. package/src/providers/key-failover.ts +5 -1
  110. package/src/providers/openai-tiers.ts +5 -0
  111. package/src/providers/provider-id-rewrite.ts +1 -0
  112. package/src/providers/quota.ts +59 -13
  113. package/src/providers/registry.ts +4 -2
  114. package/src/providers/request-pacing.ts +33 -6
  115. package/src/providers/xai-transport.ts +21 -0
  116. package/src/reasoning-effort.ts +19 -2
  117. package/src/responses/apply-patch-envelope.ts +63 -0
  118. package/src/responses/custom-tool-compat.ts +132 -38
  119. package/src/responses/google-provider-options.ts +36 -0
  120. package/src/responses/namespace-tool-compat.ts +84 -4
  121. package/src/responses/parser.ts +14 -2
  122. package/src/responses/provider-opaque-metadata.ts +3 -3
  123. package/src/responses/reasoning-replay-cache.ts +81 -3
  124. package/src/responses/schema.ts +37 -0
  125. package/src/responses/state.ts +94 -4
  126. package/src/router.ts +8 -2
  127. package/src/server/auth-cors.ts +37 -7
  128. package/src/server/images.ts +19 -35
  129. package/src/server/index.ts +102 -21
  130. package/src/server/local-management-read-client.ts +1 -1
  131. package/src/server/local-provider-reload-client.ts +1 -1
  132. package/src/server/management/agent-settings-routes.ts +206 -16
  133. package/src/server/management/combo-routes.ts +6 -0
  134. package/src/server/management/config-routes.ts +35 -6
  135. package/src/server/management/context.ts +1 -1
  136. package/src/server/management/logs-usage-routes.ts +27 -6
  137. package/src/server/management/model-routes.ts +8 -4
  138. package/src/server/management/model-rows.ts +4 -0
  139. package/src/server/management/native-integration-routes.ts +2 -1
  140. package/src/server/management/oauth-account-routes.ts +25 -4
  141. package/src/server/management/provider-capability-config.ts +1 -1
  142. package/src/server/management/provider-routes.ts +113 -15
  143. package/src/server/management/routing-profile-routes.ts +3 -0
  144. package/src/server/management/system-restart.ts +1 -1
  145. package/src/server/port-reclaim.ts +1 -1
  146. package/src/server/proxy-liveness.ts +2 -1
  147. package/src/server/request-log-conversation.ts +30 -0
  148. package/src/server/request-log.ts +21 -0
  149. package/src/server/responses/agent-task-recovery.ts +1 -1
  150. package/src/server/responses/codex-auth-error.ts +55 -0
  151. package/src/server/responses/combo-stream-preflight.ts +171 -0
  152. package/src/server/responses/compact.ts +36 -22
  153. package/src/server/responses/core.ts +584 -247
  154. package/src/server/responses/empty-completion-guard.ts +35 -6
  155. package/src/server/responses/fetch-helpers.ts +20 -102
  156. package/src/server/responses/v2-native-parent-override.ts +59 -0
  157. package/src/server/responses/ws-upstream.ts +75 -2
  158. package/src/server/responses-custom-tool-repair.ts +41 -5
  159. package/src/server/responses-undeclared-tool-guard.ts +241 -18
  160. package/src/service.ts +9 -5
  161. package/src/types/config.ts +16 -1
  162. package/src/types/provider.ts +16 -0
  163. package/src/types/request.ts +34 -1
  164. package/src/types/tools.ts +114 -11
  165. package/src/types.ts +7 -1
  166. package/src/update/index.ts +5 -4
  167. package/src/update/job.ts +3 -1
  168. package/src/update/transactional-install.mjs +8 -1
  169. package/src/usage/log.ts +16 -8
  170. package/src/usage/summary.ts +201 -8
  171. package/src/vision/describe.ts +18 -13
  172. package/src/web-search/executor.ts +10 -3
  173. package/src/web-search/gemini-executor.ts +6 -4
  174. package/src/web-search/loop.ts +42 -6
  175. package/gui/dist/assets/index-CGoDO3uO.css +0 -1
  176. package/gui/dist/assets/index-Cxt5fZMP.js +0 -102
@@ -1,6 +1,18 @@
1
1
  import type { IncomingMeta, ProviderAdapter } from "./base";
2
2
  import type { OcxParsedRequest, OcxProviderConfig } from "../types";
3
3
  import { createResponsesPassthroughAdapter } from "./openai-responses";
4
+ import { isAzureIdentityProvider } from "../config/provider-validation";
5
+ import { getAzureAccessToken } from "../lib/azure-identity";
6
+
7
+ function stripAzureAuthHeaders(headers: Record<string, string>): Record<string, string> {
8
+ const clean: Record<string, string> = {};
9
+ for (const [name, value] of Object.entries(headers)) {
10
+ const normalized = name.toLowerCase();
11
+ if (normalized === "authorization" || normalized === "api-key" || normalized === "x-api-key") continue;
12
+ clean[name] = value;
13
+ }
14
+ return clean;
15
+ }
4
16
 
5
17
  export function createAzureAdapter(provider: OcxProviderConfig): ProviderAdapter & { passthrough: true } {
6
18
  const inner = createResponsesPassthroughAdapter({
@@ -16,7 +28,8 @@ export function createAzureAdapter(provider: OcxProviderConfig): ProviderAdapter
16
28
  if (provider.authMode === "forward") {
17
29
  throw new Error("azure-openai does not support forward auth mode");
18
30
  }
19
- if (typeof provider.apiKey !== "string" || provider.apiKey.trim() === "") {
31
+ const identityMode = isAzureIdentityProvider(provider);
32
+ if (!identityMode && (typeof provider.apiKey !== "string" || provider.apiKey.trim() === "")) {
20
33
  throw new Error("azure-openai requires a non-empty apiKey");
21
34
  }
22
35
 
@@ -26,9 +39,12 @@ export function createAzureAdapter(provider: OcxProviderConfig): ProviderAdapter
26
39
  throw new Error(`azure-openai baseUrl contains unresolved ${unresolvedPlaceholder} — set your real resource URL`);
27
40
  }
28
41
 
29
- const headers = { ...request.headers };
30
- headers["api-key"] = provider.apiKey;
31
- delete headers["Authorization"];
42
+ const headers = stripAzureAuthHeaders(request.headers);
43
+ if (identityMode) {
44
+ headers.Authorization = `Bearer ${await getAzureAccessToken(provider)}`;
45
+ } else {
46
+ headers["api-key"] = provider.apiKey!;
47
+ }
32
48
  // The inner adapter always targets Azure's v1 API here, which needs no api-version query.
33
49
  return { ...request, headers };
34
50
  },
@@ -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 {
@@ -76,10 +78,12 @@ export interface AdapterRequest {
76
78
  body: string;
77
79
  /** Final upstream wire names of custom tools lowered to functions while building this request. */
78
80
  convertedRoutedCustomToolNames?: ReadonlySet<string>;
81
+ /** Native custom-tool wire names authorized for representation-only response repair. */
82
+ routedCustomToolRepairNames?: ReadonlySet<string>;
79
83
  /** Client tool-search names actually lowered to upstream function calls for this request. */
80
84
  convertedRoutedToolSearchNames?: ReadonlySet<string>;
81
85
  /** Upstream-only aliases for namespace tools flattened in this request. */
82
- convertedRoutedNamespaceToolAliases?: ReadonlyMap<string, { namespace: string; name: string }>;
86
+ convertedRoutedNamespaceToolAliases?: ReadonlyMap<string, { namespace: string; name: string; kind: "function" | "custom" }>;
83
87
  /** Releases observation of a serialized request body after its final fetch attempt settles. */
84
88
  releaseBodyObservation?: () => void;
85
89
  /** Exact reasoning parameter emitted by the adapter, for request-log diagnostics only. */
@@ -1,9 +1,9 @@
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";
5
5
  import type { AdapterEvent, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTool, OcxUsage } from "../types";
6
- import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolAllowedByChoice } from "../types";
6
+ import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolChoiceToolPredicate } from "../types";
7
7
  import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base";
8
8
  import type { TranslatorBudget } from "../lib/translator-budget";
9
9
  import { readBoundedResponseBody } from "../lib/bounded-body";
@@ -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 { EMPTY_COMMAND_CODE_PROJECT_CONTEXT, loadCommandCodeProjectContext } from "./command-code-project-context";
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;
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,8 +174,7 @@ function visibleTools(parsed: OcxParsedRequest): OcxTool[] {
157
174
  if (choice === "none") return [];
158
175
  const tools = parsed.context.tools ?? [];
159
176
  if (isAllowedToolChoice(choice)) {
160
- const allowed = new Set(choice.allowedTools);
161
- return tools.filter(tool => toolAllowedByChoice(tool, allowed, tools));
177
+ return tools.filter(toolChoiceToolPredicate(choice, tools));
162
178
  }
163
179
  if (choice && typeof choice !== "string") {
164
180
  const selected = resolveToolChoiceWireName(tools, choice.name);
@@ -211,6 +227,24 @@ function projectSlug(cwd: string): string {
211
227
  return cwd.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase().slice(0, 64) || "workspace";
212
228
  }
213
229
 
230
+ export function commandCodeSessionId(parsed: OcxParsedRequest): string {
231
+ // Shared prompt-cache cohorts intentionally do not identify one conversation. Keep them out
232
+ // of upstream session affinity or unrelated conversations can pin to the same worker.
233
+ const threadId = parsed._clientThreadId?.trim();
234
+ const replayId = parsed._reasoningReplayScope?.clientThreadId?.trim();
235
+ const cacheKey = !parsed._promptCacheKeyIsSharedCohort ? parsed.options.promptCacheKey?.trim() : undefined;
236
+ const identity = threadId
237
+ ? ["thread", threadId]
238
+ : replayId
239
+ ? ["replay", replayId]
240
+ : cacheKey
241
+ ? ["cache", cacheKey]
242
+ : undefined;
243
+ if (!identity) return randomUUID();
244
+ const hex = createHash("sha256").update(`command-code:${identity[0]}\0${identity[1]}`).digest("hex");
245
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
246
+ }
247
+
214
248
  interface GitWorkspaceInfo {
215
249
  isGitRepo: boolean;
216
250
  currentBranch: string;
@@ -452,6 +486,7 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA
452
486
  const executor = (provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? globalThis.fetch;
453
487
  return {
454
488
  name: "command-code",
489
+ formatErrorBody: formatCommandCodeErrorBody,
455
490
  async buildRequest(parsed: OcxParsedRequest): Promise<AdapterRequest> {
456
491
  if (!provider.apiKey) throw new Error("Command Code credential missing — run ocx login command-code");
457
492
  const cwd = currentWorkingDirectory();
@@ -464,11 +499,8 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA
464
499
  ...(choiceInstruction ? [choiceInstruction] : []),
465
500
  ].join("\n\n"), parsed.modelId);
466
501
  const reasoningEffort = supportedCommandCodeEffort(provider, parsed.modelId, parsed.options.reasoning);
467
- const projectContext = provider.projectContext === "on"
468
- ? await loadCommandCodeProjectContext(cwd)
469
- : EMPTY_COMMAND_CODE_PROJECT_CONTEXT;
470
502
  const body = {
471
- config: await commandCodeConfig(cwd), ...projectContext,
503
+ config: await commandCodeConfig(cwd), memory: "", taste: null, skills: null,
472
504
  permissionMode: "standard", mode: "agent",
473
505
  params: {
474
506
  model: canonicalCommandCodeModelId(parsed.modelId),
@@ -491,7 +523,7 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA
491
523
  "x-cli-environment": "production",
492
524
  "x-taste-learning": "false",
493
525
  "x-co-flag": "false",
494
- "x-session-id": randomUUID(),
526
+ "x-session-id": commandCodeSessionId(parsed),
495
527
  };
496
528
  if (cwd) headers["x-project-slug"] = projectSlug(cwd);
497
529
  return {
@@ -231,10 +231,12 @@ async function fetchCursorUsableModelsHttp2Once(opts: CursorUsableModelsOptions)
231
231
  }
232
232
 
233
233
  let status = 0;
234
+ let responseReceived = false;
234
235
  const chunks: Buffer[] = [];
235
236
  let receivedBytes = 0;
236
237
  let bodyRejected = false;
237
238
  req.on("response", headers => {
239
+ responseReceived = true;
238
240
  status = Number(headers[":status"] ?? 0);
239
241
  const contentLength = Number(headers["content-length"] ?? 0);
240
242
  if (Number.isFinite(contentLength) && contentLength > CURSOR_MODEL_DISCOVERY_MAX_BYTES) {
@@ -257,6 +259,12 @@ async function fetchCursorUsableModelsHttp2Once(opts: CursorUsableModelsOptions)
257
259
  req.on("error", () => close({ ok: false, error: "transport", detail: "HTTP/2 request failed" }));
258
260
  req.on("end", () => {
259
261
  if (bodyRejected) return;
262
+ // A stream can end after an HTTP/2 session/reset without ever delivering response headers.
263
+ // Treat status 0 as a transient transport failure so the bounded discovery retry can recover;
264
+ // reporting it as `http/HTTP unknown` makes the pre-response failure non-retryable.
265
+ if (!responseReceived || status === 0) {
266
+ return close({ ok: false, error: "transport", detail: "HTTP/2 stream ended before response headers" });
267
+ }
260
268
  if (status === 401 || status === 403) {
261
269
  return close({ ok: false, error: "auth", detail: `HTTP ${status}` });
262
270
  }
@@ -108,7 +108,7 @@ const CURSOR_STREAM_SILENCE_FAIL_MS = 30_000;
108
108
  * for this long is equally stuck — the server is alive but the turn is not progressing.
109
109
  * Reset on every decoded frame that is not liveness-only.
110
110
  */
111
- const CURSOR_STREAM_HEARTBEAT_ONLY_FAIL_MS = 90_000;
111
+ const CURSOR_STREAM_HEARTBEAT_ONLY_FAIL_MS = 180_000;
112
112
  /**
113
113
  * After `turnEnded` is decoded, the application turn is complete. A server that keeps
114
114
  * HTTP/2 open past this point cannot hold the turn hostage (senpi #1062): we close our side
@@ -122,6 +122,10 @@ function recordScreenFailure(error: string): RecordScreenResult {
122
122
  });
123
123
  }
124
124
 
125
+ function isBrokenPipe(err: unknown): boolean {
126
+ return Boolean(err && typeof err === "object" && "code" in err && (err as NodeJS.ErrnoException).code === "EPIPE");
127
+ }
128
+
125
129
  /**
126
130
  * Spawn `command` via the platform shell (sh -c on POSIX, cmd.exe /d /s /c on win32 —
127
131
  * the configured command is platform-native shell syntax; devlog
@@ -170,10 +174,22 @@ function runExternalJson(command: string, payload: unknown, config: DesktopExecu
170
174
  }
171
175
  });
172
176
 
177
+ // A child that prints and exits without reading stdin (echo, exit N) closes
178
+ // the pipe under the write. The async EPIPE is expected; wait for `close`
179
+ // and parse whatever stdout we got. An unhandled stdin error event would
180
+ // otherwise escape the Promise and fail the caller as an uncaught exception.
181
+ child.stdin.on("error", err => {
182
+ if (isBrokenPipe(err)) return;
183
+ if (settled) return;
184
+ settled = true;
185
+ clearTimeout(timer);
186
+ reject(err);
187
+ });
173
188
  try {
174
189
  child.stdin.write(JSON.stringify(payload));
175
190
  child.stdin.end();
176
191
  } catch (err) {
192
+ if (isBrokenPipe(err)) return;
177
193
  if (!settled) {
178
194
  settled = true;
179
195
  clearTimeout(timer);
@@ -176,6 +176,7 @@ export interface CursorProtobufEventState {
176
176
  */
177
177
  syntheticStructuredEditToolNames?: ReadonlySet<string>;
178
178
  translatorBudget?: TranslatorBudget;
179
+ textToolCallBuffer?: string;
179
180
  }
180
181
 
181
182
 
@@ -329,7 +330,7 @@ function isCompleteJson(text: string): boolean {
329
330
 
330
331
  /** Schema-normalize a JSON-text argument blob for a named tool, if a schema is known. */
331
332
  function normalizeJsonText(text: string, toolName: string | undefined, state: CursorProtobufEventState): string {
332
- const schema = toolSchemaForWireName(state, toolName);
333
+ const schema = toolSchemaForWireName(state, toolName) ?? (toolName ? defaultShellBridgeArgNormalizeSchema(toolName) : undefined);
333
334
  if (!schema) return text;
334
335
  try {
335
336
  const parsed = JSON.parse(text);
@@ -1240,14 +1241,159 @@ export function mapCursorProtobufServerMessage(
1240
1241
  return [];
1241
1242
  }
1242
1243
 
1244
+ const TOOL_CALL_START_PREFIX = "[TOOL_CALL]";
1245
+ const TOOL_CALL_ARGS_MARKER = "[ARGS]";
1246
+
1247
+ function findPotentialMarkerPrefix(text: string): number {
1248
+ for (let len = Math.min(TOOL_CALL_START_PREFIX.length - 1, text.length); len >= 1; len--) {
1249
+ const candidate = text.slice(text.length - len);
1250
+ if (TOOL_CALL_START_PREFIX.startsWith(candidate)) {
1251
+ return text.length - len;
1252
+ }
1253
+ }
1254
+ return -1;
1255
+ }
1256
+
1257
+ function parseCursorTextToolCalls(text: string, state: CursorProtobufEventState): CursorServerMessage[] {
1258
+ if (!state.clientToolNames) {
1259
+ return text ? [{ type: "text", text: normalizeCursorTextToolMarkers(text) }] : [];
1260
+ }
1261
+
1262
+ let remaining = (state.textToolCallBuffer ?? "") + text;
1263
+ state.textToolCallBuffer = undefined;
1264
+
1265
+ if (!remaining) return [];
1266
+
1267
+ const out: CursorServerMessage[] = [];
1268
+
1269
+ while (remaining.length > 0) {
1270
+ const startIndex = remaining.indexOf(TOOL_CALL_START_PREFIX);
1271
+ if (startIndex === -1) {
1272
+ const partialIndex = findPotentialMarkerPrefix(remaining);
1273
+ if (partialIndex !== -1) {
1274
+ if (partialIndex > 0) {
1275
+ out.push({ type: "text", text: normalizeCursorTextToolMarkers(remaining.slice(0, partialIndex)) });
1276
+ }
1277
+ state.textToolCallBuffer = remaining.slice(partialIndex);
1278
+ break;
1279
+ }
1280
+ out.push({ type: "text", text: normalizeCursorTextToolMarkers(remaining) });
1281
+ break;
1282
+ }
1283
+
1284
+ // Check if the candidate at startIndex is actually a tool call
1285
+ const candidateAfter = remaining.slice(startIndex + TOOL_CALL_START_PREFIX.length);
1286
+ const argsPos = candidateAfter.indexOf(TOOL_CALL_ARGS_MARKER);
1287
+ const namePart = argsPos !== -1 ? candidateAfter.slice(0, argsPos).trim() : candidateAfter;
1288
+ const isCandidateValid = namePart.length > 0 && namePart.length <= 64 && /^[a-zA-Z0-9_-]+$/.test(namePart);
1289
+
1290
+ if (!isCandidateValid && argsPos === -1) {
1291
+ // Not a valid tool call and no [ARGS]; treat as plain text
1292
+ out.push({ type: "text", text: normalizeCursorTextToolMarkers(remaining) });
1293
+ break;
1294
+ }
1295
+
1296
+ if (startIndex > 0) {
1297
+ out.push({ type: "text", text: normalizeCursorTextToolMarkers(remaining.slice(0, startIndex)) });
1298
+ remaining = remaining.slice(startIndex);
1299
+ continue;
1300
+ }
1301
+
1302
+ const argsMarkerIndex = remaining.indexOf(TOOL_CALL_ARGS_MARKER, TOOL_CALL_START_PREFIX.length);
1303
+ if (argsMarkerIndex === -1) {
1304
+ const nameCandidate = remaining.slice(TOOL_CALL_START_PREFIX.length);
1305
+ if (nameCandidate.length > 64 || /[^a-zA-Z0-9_-]/.test(nameCandidate)) {
1306
+ out.push({ type: "text", text: normalizeCursorTextToolMarkers(remaining) });
1307
+ break;
1308
+ }
1309
+ state.textToolCallBuffer = remaining;
1310
+ break;
1311
+ }
1312
+
1313
+ const rawName = remaining.slice(TOOL_CALL_START_PREFIX.length, argsMarkerIndex).trim();
1314
+ const wireName = normalizeCursorWireName(rawName);
1315
+ const advertisedName = resolveAdvertisedClientToolName(state, wireName);
1316
+
1317
+ if (!advertisedName) {
1318
+ const endOfMarker = argsMarkerIndex + TOOL_CALL_ARGS_MARKER.length;
1319
+ out.push({ type: "text", text: normalizeCursorTextToolMarkers(remaining.slice(0, endOfMarker)) });
1320
+ remaining = remaining.slice(endOfMarker);
1321
+ continue;
1322
+ }
1323
+
1324
+ const argsStart = argsMarkerIndex + TOOL_CALL_ARGS_MARKER.length;
1325
+ let argsEnd = -1;
1326
+ const trimmedOffset = remaining.slice(argsStart).search(/\S/);
1327
+ if (trimmedOffset !== -1 && remaining[argsStart + trimmedOffset] === "{") {
1328
+ let depth = 0;
1329
+ let inString = false;
1330
+ let escape = false;
1331
+ const jsonStart = argsStart + trimmedOffset;
1332
+ for (let i = jsonStart; i < remaining.length; i++) {
1333
+ const char = remaining[i];
1334
+ if (escape) {
1335
+ escape = false;
1336
+ continue;
1337
+ }
1338
+ if (char === "\\") {
1339
+ escape = true;
1340
+ continue;
1341
+ }
1342
+ if (char === '"') {
1343
+ inString = !inString;
1344
+ continue;
1345
+ }
1346
+ if (!inString) {
1347
+ if (char === "{") depth++;
1348
+ else if (char === "}") {
1349
+ depth--;
1350
+ if (depth === 0) {
1351
+ argsEnd = i + 1;
1352
+ break;
1353
+ }
1354
+ }
1355
+ }
1356
+ }
1357
+ }
1358
+
1359
+ if (argsEnd === -1) {
1360
+ state.textToolCallBuffer = remaining;
1361
+ break;
1362
+ }
1363
+
1364
+ const argsJson = remaining.slice(argsStart, argsEnd).trim();
1365
+ const toolCallId = `call_${crypto.randomUUID().replace(/-/g, "").slice(0, 16)}`;
1366
+
1367
+ const recordEvents = recordToolCall(state, toolCallId, wireName);
1368
+ out.push(...recordEvents);
1369
+ if (!recordEvents.some(e => e.type === "error")) {
1370
+ const open = state.openToolCalls.get(toolCallId);
1371
+ if (open) open.args = argsJson;
1372
+ let finalArgs = normalizeJsonText(argsJson, wireName, state);
1373
+ if (state.freeformToolNames?.has(open?.name ?? "") && !cursorFreeformWrapperValid(finalArgs)) {
1374
+ try {
1375
+ const parsed = JSON.parse(finalArgs) as unknown;
1376
+ if (typeof parsed === "string") {
1377
+ finalArgs = JSON.stringify({ input: parsed });
1378
+ }
1379
+ } catch {
1380
+ finalArgs = JSON.stringify({ input: finalArgs });
1381
+ }
1382
+ }
1383
+ out.push(...commitToolCall(state, toolCallId, finalArgs));
1384
+ }
1385
+
1386
+ remaining = remaining.slice(argsEnd);
1387
+ }
1388
+
1389
+ return out;
1390
+ }
1391
+
1243
1392
  if (serverMessage.message.case !== "interactionUpdate") return [];
1244
1393
  const update = serverMessage.message.value.message;
1245
1394
  switch (update.case) {
1246
1395
  case "textDelta":
1247
- // #2305: fold Cursor display aliases inside textual pseudo tool-call markers back to
1248
- // the advertised wire name before any client sees the text. Real frames are already
1249
- // normalized structurally (mcpWireNameFromArgs above).
1250
- return update.value.text ? [{ type: "text", text: normalizeCursorTextToolMarkers(update.value.text) }] : [];
1396
+ return update.value.text ? parseCursorTextToolCalls(update.value.text, state) : [];
1251
1397
  case "thinkingDelta":
1252
1398
  return update.value.text ? [{ type: "thinking", thinking: update.value.text }] : [];
1253
1399
  case "toolCallStarted": {
@@ -1364,18 +1510,23 @@ export function resolvedTurnUsage(state: CursorProtobufEventState): OcxUsage {
1364
1510
  */
1365
1511
  export function finalizeTurnEvents(state: CursorProtobufEventState): CursorServerMessage[] {
1366
1512
  state.terminated = true;
1513
+ const prefixEvents: CursorServerMessage[] = [];
1514
+ if (state.textToolCallBuffer) {
1515
+ prefixEvents.push({ type: "text", text: normalizeCursorTextToolMarkers(state.textToolCallBuffer) });
1516
+ state.textToolCallBuffer = undefined;
1517
+ }
1367
1518
  if (state.openToolCalls.size > 0) {
1368
1519
  const openCallIds = [...state.openToolCalls.keys()];
1369
1520
  const openIds = openCallIds.join(", ");
1370
1521
  // Clear so a second turnEnded (should not happen, but defensive) doesn't re-emit.
1371
1522
  for (const callId of openCallIds) state.translatorBudget?.closeCall(callId);
1372
1523
  state.openToolCalls.clear();
1373
- return [{ type: "error", message: `Cursor stream ended with incomplete tool call(s): ${openIds}. Arguments may be truncated; the call was not committed.` }];
1524
+ return [...prefixEvents, { type: "error", message: `Cursor stream ended with incomplete tool call(s): ${openIds}. Arguments may be truncated; the call was not committed.` }];
1374
1525
  }
1375
1526
  // Surface the absolute context size (when Cursor reported a checkpoint) as both totalTokens and
1376
1527
  // the estimated input side of Codex's visible `input + output` counter. Codex status lines can
1377
1528
  // render the additive pair instead of total_tokens, so leaving inputTokens at 0 makes a 16k-context
1378
1529
  // first turn display as "9 used". Keep outputTokens as the per-turn delta and clamp the inferred
1379
1530
  // input to 0 in case Cursor reports a checkpoint smaller than the streamed output delta.
1380
- return [{ type: "done", usage: resolvedTurnUsage(state) }];
1531
+ return [...prefixEvents, { type: "done", usage: resolvedTurnUsage(state) }];
1381
1532
  }
@@ -1,7 +1,7 @@
1
1
  import { create, fromBinary, toBinary, toJson } from "@bufbuild/protobuf";
2
2
  import { fromJson, type JsonValue } from "@bufbuild/protobuf";
3
3
  import { ValueSchema } from "@bufbuild/protobuf/wkt";
4
- import type { OcxAssistantContentPart, OcxMessage, OcxToolResultMessage } from "../../types";
4
+ import type { OcxAssistantContentPart, OcxMessage, OcxRequestOptions, OcxToolResultMessage } from "../../types";
5
5
  import { namespacedToolName } from "../../types";
6
6
  import type { CursorRunRequest } from "./types";
7
7
  import { cursorNeedsExternalToolContinuation, isCursorExternalWireModel } from "./discovery";
@@ -165,9 +165,26 @@ function truncateToolResultBlob(entry: RootBlobCandidate, maxBytes: number): Roo
165
165
  return markerOnly.byteLength <= maxBytes ? markerOnly : null;
166
166
  }
167
167
 
168
+ function structuredOutputPrompt(textFormat: OcxRequestOptions["textFormat"]): string | undefined {
169
+ if (!textFormat) return undefined;
170
+ if (textFormat.type === "json_schema" && textFormat.schema) {
171
+ return [
172
+ "Your response must be a single valid JSON object strictly conforming to this JSON schema:",
173
+ JSON.stringify(textFormat.schema),
174
+ "Do not include any surrounding markdown fences, preamble, or commentary; return raw JSON only.",
175
+ ].join("\n");
176
+ }
177
+ if (textFormat.type === "json_object") {
178
+ return "Your response must be a single valid JSON object. Do not include any markdown fences or commentary; return raw JSON only.";
179
+ }
180
+ return undefined;
181
+ }
182
+
168
183
  function systemPromptBlobs(request: CursorRunRequest): RootBlobCandidate[] {
169
184
  const prompts = request.system.length > 0 ? [...request.system] : ["You are a helpful assistant."];
170
185
  if (cursorRequestHasShellAlias(request.tools)) prompts.push(CURSOR_SHELL_ALIAS_SYSTEM_NOTE);
186
+ const structuredPrompt = structuredOutputPrompt(request.textFormat);
187
+ if (structuredPrompt) prompts.push(structuredPrompt);
171
188
  const cursorToolGuidance = buildCursorToolGuidanceSystemNote(
172
189
  cursorToolsForActivePrompt(request.tools, activePromptText(request), request.toolChoice),
173
190
  request.toolChoice,
@@ -190,10 +207,11 @@ function assistantRootText(
190
207
  // Cursor builds the actual model prompt from rootPromptMessagesJson (turns[] is UI/display metadata),
191
208
  // so prior history must be replayed here or a ResumeAction has nothing model-visible to continue from.
192
209
  // The active user message is excluded because it travels in the action. When the continuation cannot
193
- // rely on native MCP turn state, tool results stay assistant-role text with a [Tool Result] /
194
- // [Tool Error] marker so Cursor does not wrap them as `<user_query>` (#1992). Native resume models
195
- // already carry the paired MCP result on turns[], so that marker is omitted from root replay — Auto
196
- // few-shot-mimics it as chat text otherwise. Each entry is a SHA-256 blob ID.
210
+ // rely on native MCP turn state, tool results stay assistant-role text so Cursor does not wrap them
211
+ // as `<user_query>` (#1992). External replay uses a neutral "Tool output" label; protocol markers
212
+ // such as [Tool Result] are reserved for native wire encoding because external models echo them.
213
+ // Native resume models already carry the paired MCP result on turns[], so it is omitted from root
214
+ // replay. Each entry is a SHA-256 blob ID.
197
215
  function rootPromptMessages(request: CursorRunRequest, requestScope: CursorBlobRequestScopeToken): {
198
216
  ids: Uint8Array[];
199
217
  byteLength: number;
@@ -246,14 +264,10 @@ function rootPromptMessages(request: CursorRunRequest, requestScope: CursorBlobR
246
264
  }
247
265
  // Assistant tool CALLS are intentionally NOT replayed as visible "[Tool Call]" text here.
248
266
  } else if (message.role === "toolResult") {
249
- // Native resume models already receive the paired MCP result through turns[]. Replaying
250
- // the same payload as assistant-role "[Tool Result]" / "[tool_result]" text teaches Auto
251
- // to echo that envelope as chat instead of continuing from the structured result.
267
+ // Native resume models already receive the paired MCP result through turns[]. External
268
+ // replay uses neutral text here so models do not echo protocol envelopes as chat.
252
269
  if (!echoToolResultInRoot) continue;
253
- // #1920: the prefix must reflect the NORMALIZED error state (an empty
254
- // node_repl result is an error even when the runtime said isError=false).
255
- const prefix = normalizedToolResult(message, contentToText(message.content)).isError ? "[Tool Error]" : "[Tool Result]";
256
- const text = `${prefix}\n${toolResultToText(message)}`;
270
+ const text = externalToolResultToText(message);
257
271
  entries.push(rootBlobCandidate(
258
272
  toolResultRootPayload(text),
259
273
  "toolResult",
@@ -538,6 +552,12 @@ function toolResultToText(message: OcxToolResultMessage): string {
538
552
  ].join("\n");
539
553
  }
540
554
 
555
+ function externalToolResultToText(message: OcxToolResultMessage): string {
556
+ const normalized = normalizedToolResult(message, contentToText(message.content));
557
+ const label = normalized.isError ? "Tool error" : "Tool output";
558
+ return `${label} for ${namespacedToolName(message.toolNamespace, message.toolName)} (call_id: ${message.toolCallId}, is_error: ${normalized.isError}):\n${normalized.text}`;
559
+ }
560
+
541
561
  /**
542
562
  * Shared #1920 normalization entry: pure-text results only. Image-bearing or
543
563
  * encrypted results pass through untouched (their content is not plain text).
@@ -721,12 +741,10 @@ function conversationTurns(
721
741
  // #1920/#1866: this external-replay site bypasses toolResultToText, so it
722
742
  // must consume the normalizer directly — cursor/grok-4.6 is the exact
723
743
  // reported repro path for empty Computer Use results.
724
- const normalized = normalizedToolResult(message, contentToText(message.content));
725
- const prefix = normalized.isError ? "[Tool Error]" : "[Tool Result]";
726
744
  current.steps.push(storeCursorBlob(toBinary(ConversationStepSchema, create(ConversationStepSchema, {
727
745
  message: {
728
746
  case: "assistantMessage",
729
- value: create(AssistantMessageSchema, { text: `${prefix}\n${normalized.text}` }),
747
+ value: create(AssistantMessageSchema, { text: externalToolResultToText(message) }),
730
748
  },
731
749
  })), requestScope));
732
750
  continue;
@@ -442,9 +442,10 @@ export function createCursorRequest(
442
442
  rawMessages: parsed.context.messages,
443
443
  ...(parsed._compactionRequest === true || parsed._contextCompactionBoundary === true ? { contextUsageReset: true } : {}),
444
444
  ...(parsed._compactionRequest === true ? { contextUsageStoreCheckpoints: false } : {}),
445
- ...(budget.tools.length ? { tools: budget.tools } : {}),
446
- ...(parsed.options.toolChoice ? { toolChoice: parsed.options.toolChoice } : {}),
447
- ...(parsed.options.parallelToolCalls !== undefined ? { parallelToolCalls: parsed.options.parallelToolCalls } : {}),
445
+ ...(budget.tools.length ? { tools: budget.tools } : {}),
446
+ ...(parsed.options.toolChoice ? { toolChoice: parsed.options.toolChoice } : {}),
447
+ ...(parsed.options.textFormat ? { textFormat: parsed.options.textFormat } : {}),
448
+ ...(parsed.options.parallelToolCalls !== undefined ? { parallelToolCalls: parsed.options.parallelToolCalls } : {}),
448
449
  };
449
450
  const resolved = resolveCursorCheckpoint(parsed, request, options);
450
451
  if ("reason" in resolved) {
@@ -48,6 +48,24 @@ export const CURSOR_EXEC_COMMAND_INPUT_SCHEMA = {
48
48
  tty: { type: "boolean", description: "True allocates a PTY for the command; false or omitted uses plain pipes." },
49
49
  yield_time_ms: { type: "number", description: "Wait before yielding output. Defaults to 10000 ms; effective range is 250-30000 ms." },
50
50
  max_output_tokens: { type: "number", description: "Output token budget. Defaults to 10000 tokens; larger requests may be capped by policy." },
51
+ sandbox_permissions: {
52
+ type: "string",
53
+ enum: ["use_default", "require_escalated"],
54
+ description: "Per-command sandbox override. Defaults to use_default; use require_escalated for unsandboxed execution.",
55
+ },
56
+ justification: {
57
+ type: "string",
58
+ description: "User-facing approval question for require_escalated; omit otherwise.",
59
+ },
60
+ prefix_rule: {
61
+ type: "array",
62
+ items: { type: "string" },
63
+ description: "Reusable approval prefix for cmd, only with sandbox_permissions: require_escalated; for example [\"git\", \"pull\"].",
64
+ },
65
+ login: {
66
+ type: "boolean",
67
+ description: "True runs the shell with -l/-i semantics; false disables them. Defaults to true.",
68
+ },
51
69
  },
52
70
  required: ["cmd"],
53
71
  additionalProperties: false,
@@ -109,6 +127,10 @@ export const CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA = {
109
127
  yield_time_ms: { type: "number", description: "Wait before yielding output. Defaults to 10000 ms; effective range is 250-30000 ms." },
110
128
  max_output_tokens: { type: "number", description: "Output token budget. Defaults to 10000 tokens; larger requests may be capped by policy." },
111
129
  max_output_chars: { type: "number", description: "Output character budget when the Responses tool uses chars instead of tokens." },
130
+ sandbox_permissions: { type: "string" },
131
+ justification: { type: "string" },
132
+ prefix_rule: { type: "array" },
133
+ login: { type: "boolean" },
112
134
  },
113
135
  required: ["command"],
114
136
  } as const;
@@ -651,7 +673,7 @@ export function buildCursorToolGuidanceSystemNote(
651
673
  // Code mode: shell/edit/MCP live inside freeform `exec` as nested helpers. Without this the
652
674
  // model probes for a top-level shell tool that is not there.
653
675
  codeMode
654
- ? `\`${CODEX_UNIFIED_EXEC_TOOL}\` is Codex code mode: its body is JavaScript evaluated in a V8 isolate, not a shell command and not Node. Shell, file edits, and MCP are nested helpers called INSIDE that body as \`await tools.<name>(...)\`, for example \`await tools.exec_command({cmd: \"ls\"})\`. Read the tool description and the isolate global \`ALL_TOOLS\` (not \`tools.ALL_TOOLS\`) for helpers this turn provides; absence from the top-level catalog or from \`exec\`'s description is not absence. Those nested helpers are not themselves top-level tools, so do not call \`exec_command\` or \`shell_command\` at the top level here${codeModeOtherTopLevelNames.length > 0 ? `; every other tool this turn lists, including ${quotedNames(codeModeOtherTopLevelNames)}, remains callable at the top level as usual` : ""}.`
676
+ ? `\`${CODEX_UNIFIED_EXEC_TOOL}\` is Codex code mode: its body is JavaScript evaluated in a V8 isolate, not a shell command and not Node. Shell, file edits, and MCP are nested helpers called INSIDE that body as \`await tools.<name>(...)\`, for example \`await tools.exec_command({cmd: \"ls\"})\`. When commands require network access, file writes outside workspace, or fail due to sandbox/permission restrictions, pass \`sandbox_permissions: "require_escalated"\` and a clear \`justification: "..."\` to \`tools.exec_command\`. Read the tool description and the isolate global \`ALL_TOOLS\` (not \`tools.ALL_TOOLS\`) for helpers this turn provides; absence from the top-level catalog or from \`exec\`'s description is not absence. Those nested helpers are not themselves top-level tools, so do not call \`exec_command\` or \`shell_command\` at the top level here${codeModeOtherTopLevelNames.length > 0 ? `; every other tool this turn lists, including ${quotedNames(codeModeOtherTopLevelNames)}, remains callable at the top level as usual` : ""}. Your tool list may display tools under a longer \`mcp_opencodex-responses_*\` name; call whichever your list shows. Nested \`tools.apply_patch(input)\` is host-executed: the string must begin exactly with \`*** Begin Patch\` and end with \`*** End Patch\` (no trailing \`***\` on those lines). OpenCodex does not rewrite JavaScript inside exec, so a decorated \`*** Begin Patch ***\` envelope is rejected by Codex before the file is touched.`
655
677
  : undefined,
656
678
  codeMode
657
679
  ? "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."
@@ -665,6 +687,9 @@ export function buildCursorToolGuidanceSystemNote(
665
687
  hasBareExec
666
688
  ? `Prefer the Codex shell bridge over Cursor-native Shell/Read. If a Cursor-native file read, directory listing, grep, or shell operation is rejected, continue with the listed catalog tool ${shellBridgeLabel}.`
667
689
  : undefined,
690
+ hasBareExec
691
+ ? 'When a command requires network access, file writes outside workspace, or fails due to sandbox restrictions, include `sandbox_permissions: "require_escalated"` and `justification: "..."`.'
692
+ : undefined,
668
693
  hostShellNote,
669
694
  "Cursor product features (Chronicle, screen recording, Notes, Plans, background agents) are available only if this turn's catalog lists a matching tool; do not offer or promise them otherwise.",
670
695
  hasBareExec
@@ -686,6 +711,7 @@ export function buildCursorToolGuidanceSystemNote(
686
711
  ? `Use ${discoveryTools} only for explicit discovery/resource tasks, not generic tool-count demos.`
687
712
  : undefined,
688
713
  "Do not count or report a tool call unless a tool result was actually returned.",
714
+ "When pursuing a multi-step task, check, or verification, do not stop or narrate intended future actions in plain text; immediately call the tool to execute the next step until the task is complete.",
689
715
  hasBareExec
690
716
  ? `If a Cursor-native file read, directory listing, grep, or shell operation is rejected by the runtime, use ${shellBridgeLabel} with an equivalent host-shell-safe command (POSIX: \`cat\`/\`ls\`/\`rg\`; Windows PowerShell: \`Get-Content\`/\`Get-ChildItem\`/\`Select-String\`). For file edits, use ${structuredEditNames.length > 0 ? `the structured edit tools (${quotedNames(structuredEditNames)}) or ` : ""}\`apply_patch\` when available.`
691
717
  : undefined,
@@ -25,9 +25,10 @@ export interface CursorRunRequest {
25
25
  * hydration). History stays text-only. data: URLs only in this slice.
26
26
  */
27
27
  selectedImages?: readonly ResolvedCursorImage[];
28
- tools?: OcxTool[];
29
- toolChoice?: OcxRequestOptions["toolChoice"];
30
- parallelToolCalls?: boolean;
28
+ tools?: OcxTool[];
29
+ toolChoice?: OcxRequestOptions["toolChoice"];
30
+ textFormat?: OcxRequestOptions["textFormat"];
31
+ parallelToolCalls?: boolean;
31
32
  /**
32
33
  * Clear provider-private context-usage carry-forward before this run. Used when Codex starts a
33
34
  * newly observed compacted context epoch, so pre-compaction totals are not over-reported while