@yansigit/opencodex 2.33.0 → 2.35.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 (196) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/index-BjCaHxdz.js +112 -0
  3. package/gui/dist/assets/index-DLkXOXLC.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/anthropic.ts +79 -2
  7. package/src/adapters/command-code.ts +141 -23
  8. package/src/adapters/cursor/call-id.ts +44 -0
  9. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  10. package/src/adapters/cursor/discovery.ts +60 -2
  11. package/src/adapters/cursor/effort-map.ts +79 -1
  12. package/src/adapters/cursor/envelope-echo.ts +162 -0
  13. package/src/adapters/cursor/live-models.ts +7 -2
  14. package/src/adapters/cursor/live-transport.ts +17 -1
  15. package/src/adapters/cursor/message-mapper.ts +4 -1
  16. package/src/adapters/cursor/native-exec-fs.ts +13 -12
  17. package/src/adapters/cursor/native-exec-network.ts +3 -5
  18. package/src/adapters/cursor/native-exec-policy.ts +47 -0
  19. package/src/adapters/cursor/native-exec-shell.ts +116 -31
  20. package/src/adapters/cursor/native-exec.ts +38 -10
  21. package/src/adapters/cursor/protobuf-events.ts +28 -2
  22. package/src/adapters/cursor/protobuf-request.ts +93 -41
  23. package/src/adapters/cursor/request-builder.ts +39 -10
  24. package/src/adapters/cursor/tool-definitions.ts +27 -3
  25. package/src/adapters/cursor/tool-result-normalize.ts +51 -6
  26. package/src/adapters/cursor/types.ts +23 -4
  27. package/src/adapters/cursor.ts +170 -29
  28. package/src/adapters/google-aistudio-parser.ts +49 -0
  29. package/src/adapters/google-antigravity-replay.ts +105 -25
  30. package/src/adapters/google-antigravity-wire.ts +5 -0
  31. package/src/adapters/google-errors.ts +41 -12
  32. package/src/adapters/google-http.ts +12 -11
  33. package/src/adapters/google.ts +219 -36
  34. package/src/adapters/image.ts +1 -1
  35. package/src/adapters/kiro-constants.ts +15 -0
  36. package/src/adapters/kiro-tools.ts +43 -15
  37. package/src/adapters/kiro.ts +54 -9
  38. package/src/adapters/openai-chat.ts +286 -242
  39. package/src/adapters/openai-responses.ts +335 -24
  40. package/src/adapters/run-turn-queue.ts +36 -1
  41. package/src/adapters/tool-catalog-nudge.ts +2 -2
  42. package/src/adapters/xai-tool-schema.ts +436 -0
  43. package/src/bridge.ts +67 -26
  44. package/src/chat/inbound.ts +29 -1
  45. package/src/chat/outbound.ts +15 -7
  46. package/src/claude/agents-inject.ts +8 -1
  47. package/src/claude/outbound.ts +10 -8
  48. package/src/cli/account-api.ts +27 -7
  49. package/src/cli/account-extended.ts +10 -3
  50. package/src/cli/account.ts +29 -5
  51. package/src/cli/alias.ts +66 -0
  52. package/src/cli/claude.ts +26 -1
  53. package/src/cli/dispatch.ts +13 -1
  54. package/src/cli/help.ts +1 -0
  55. package/src/cli/index.ts +6 -1
  56. package/src/cli/init.ts +1 -0
  57. package/src/cli/models-runtime.ts +95 -0
  58. package/src/cli/models.ts +13 -7
  59. package/src/cli/provider-runtime.ts +16 -2
  60. package/src/cli/registry.ts +6 -1
  61. package/src/cli/telemetry-commands.ts +25 -0
  62. package/src/cli/v2.ts +34 -10
  63. package/src/codex/account-pause.ts +2 -1
  64. package/src/codex/account-priority.ts +3 -2
  65. package/src/codex/app-server-processes.ts +80 -6
  66. package/src/codex/auth-api.ts +48 -8
  67. package/src/codex/auth-context.ts +21 -18
  68. package/src/codex/catalog/aggregation.ts +6 -0
  69. package/src/codex/catalog/model-metadata.ts +13 -1
  70. package/src/codex/catalog/native-models.ts +5 -2
  71. package/src/codex/catalog/parsing.ts +16 -0
  72. package/src/codex/catalog/provider-fetch.ts +20 -3
  73. package/src/codex/catalog/sync.ts +127 -2
  74. package/src/codex/catalog.ts +1 -1
  75. package/src/codex/codex-write-lock.ts +3 -1
  76. package/src/codex/convergence-types.ts +1 -1
  77. package/src/codex/convergence.ts +22 -2
  78. package/src/codex/desired-state.ts +2 -2
  79. package/src/codex/desktop-app-restart.ts +18 -5
  80. package/src/codex/inject-coordination.ts +83 -0
  81. package/src/codex/inject.ts +14 -1
  82. package/src/codex/log-guard/inspect.ts +22 -4
  83. package/src/codex/model-entitlements.ts +9 -2
  84. package/src/codex/prompt-layers.ts +371 -25
  85. package/src/codex/prompt-text-probe.ts +238 -0
  86. package/src/codex/quota.ts +123 -18
  87. package/src/codex/routing.ts +9 -0
  88. package/src/codex/subagent-model-fallback.ts +198 -27
  89. package/src/codex/transition-state.ts +107 -8
  90. package/src/combos/types.ts +10 -0
  91. package/src/compatibility/openai-responses.ts +33 -1
  92. package/src/config/autonomous-remediation.ts +21 -0
  93. package/src/config/provider-validation.ts +14 -0
  94. package/src/config/rebase-provenance.ts +68 -0
  95. package/src/config.ts +191 -17
  96. package/src/generated/compatibility-version.json +279 -159
  97. package/src/generated/model-metadata.ts +3 -0
  98. package/src/images/loop.ts +5 -4
  99. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  100. package/src/lab/fabric/producer-child.ts +1 -1
  101. package/src/lib/config-ownership.ts +20 -0
  102. package/src/lib/errors.ts +11 -2
  103. package/src/lib/package-tree-integrity.ts +101 -0
  104. package/src/oauth/aistudio-credentials.ts +65 -0
  105. package/src/oauth/aistudio-native-daemon.ts +116 -0
  106. package/src/oauth/aistudio-session-sync.ts +95 -0
  107. package/src/oauth/generic-account-failover.ts +231 -0
  108. package/src/oauth/google-aistudio-auth.ts +98 -0
  109. package/src/oauth/index.ts +57 -5
  110. package/src/oauth/key-providers.ts +18 -1
  111. package/src/oauth/kiro.ts +45 -0
  112. package/src/oauth/login-cli.ts +65 -1
  113. package/src/oauth/types.ts +15 -0
  114. package/src/providers/codex-capacity.ts +5 -2
  115. package/src/providers/command-code-efforts.ts +38 -6
  116. package/src/providers/context-cap.ts +4 -3
  117. package/src/providers/default-aliases.ts +65 -0
  118. package/src/providers/derive.ts +29 -1
  119. package/src/providers/fastwire.ts +7 -1
  120. package/src/providers/model-presets.ts +119 -0
  121. package/src/providers/new-model-policy.ts +146 -0
  122. package/src/providers/provider-id-rewrite.ts +2 -1
  123. package/src/providers/quota.ts +157 -46
  124. package/src/providers/registry.ts +184 -71
  125. package/src/providers/slug-codec.ts +52 -0
  126. package/src/responses/code-mode-helper-compat.ts +50 -0
  127. package/src/responses/custom-tool-compat.ts +34 -10
  128. package/src/responses/parser.ts +4 -0
  129. package/src/responses/schema.ts +5 -1
  130. package/src/responses/thought-signature-replay.ts +17 -0
  131. package/src/router.ts +43 -2
  132. package/src/routing/account-pool/cooldown.ts +8 -0
  133. package/src/routing/account-pool/index.ts +1 -0
  134. package/src/routing/analytics.ts +1 -0
  135. package/src/routing/quota.ts +10 -0
  136. package/src/server/auth-cors.ts +24 -0
  137. package/src/server/chat-completions.ts +26 -16
  138. package/src/server/chat-native-sse.ts +3 -3
  139. package/src/server/chat-native.ts +30 -11
  140. package/src/server/claude-messages.ts +1 -1
  141. package/src/server/effort-policy.ts +16 -0
  142. package/src/server/index.ts +180 -14
  143. package/src/server/lifecycle.ts +52 -1
  144. package/src/server/management/agent-settings-routes.ts +31 -15
  145. package/src/server/management/codex-prompt-routes.ts +570 -0
  146. package/src/server/management/combo-routes.ts +2 -1
  147. package/src/server/management/config-routes.ts +27 -9
  148. package/src/server/management/context.ts +9 -0
  149. package/src/server/management/logs-usage-routes.ts +11 -5
  150. package/src/server/management/model-routes.ts +266 -0
  151. package/src/server/management/oauth-account-routes.ts +13 -3
  152. package/src/server/management/provider-routes.ts +137 -3
  153. package/src/server/management/routing-profile-routes.ts +2 -2
  154. package/src/server/management-api.ts +2 -0
  155. package/src/server/port-reclaim.ts +19 -1
  156. package/src/server/relay-eager.ts +147 -20
  157. package/src/server/relay.ts +251 -19
  158. package/src/server/request-log-conversation.ts +33 -0
  159. package/src/server/request-log.ts +48 -21
  160. package/src/server/responses/collaboration.ts +42 -5
  161. package/src/server/responses/combo-stream-preflight.ts +10 -3
  162. package/src/server/responses/core.ts +575 -140
  163. package/src/server/responses/empty-completion-guard.ts +35 -0
  164. package/src/server/responses/fetch-helpers.ts +14 -6
  165. package/src/server/responses/input-admission.ts +3 -1
  166. package/src/server/responses/passthrough-error.ts +33 -9
  167. package/src/server/responses/policy-fallback.ts +1 -1
  168. package/src/server/responses/responses-field-backfill.ts +105 -13
  169. package/src/server/responses/ws-upstream.ts +35 -5
  170. package/src/server/responses-custom-tool-repair.ts +52 -7
  171. package/src/server/responses-terminal-repair.ts +25 -4
  172. package/src/server/sse-frame-buffer.ts +31 -4
  173. package/src/server/ws-bridge.ts +14 -2
  174. package/src/smoke/fingerprint-cache.ts +133 -0
  175. package/src/smoke/live-scenarios.ts +33 -0
  176. package/src/smoke/runner.ts +119 -0
  177. package/src/telemetry/dispatcher.ts +44 -0
  178. package/src/telemetry/fingerprint.ts +24 -0
  179. package/src/telemetry/hook.ts +43 -0
  180. package/src/telemetry/ledger.ts +54 -0
  181. package/src/telemetry/types.ts +23 -0
  182. package/src/types/config.ts +66 -14
  183. package/src/types/provider.ts +79 -1
  184. package/src/types/request.ts +18 -10
  185. package/src/types/tools.ts +30 -11
  186. package/src/types.ts +1 -0
  187. package/src/usage/command-code-manifest.ts +116 -0
  188. package/src/usage/cost.ts +2 -2
  189. package/src/usage/expected-prices.ts +126 -24
  190. package/src/usage/log.ts +18 -8
  191. package/src/usage/summary.ts +34 -12
  192. package/src/web-search/exa-executor.ts +40 -9
  193. package/src/web-search/index.ts +16 -8
  194. package/src/web-search/loop.ts +5 -4
  195. package/gui/dist/assets/index-DKLr4LTE.js +0 -102
  196. package/gui/dist/assets/index-DrSQdTRd.css +0 -1
@@ -1,4 +1,4 @@
1
- import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from "node:child_process";
1
+ import { execFileSync, spawn, spawnSync, type ChildProcessWithoutNullStreams } from "node:child_process";
2
2
  import { resolve } from "node:path";
3
3
  import { create } from "@bufbuild/protobuf";
4
4
  import {
@@ -19,6 +19,9 @@ import {
19
19
  type ExecServerMessage,
20
20
  } from "./gen/agent_pb";
21
21
  import { errorText, execBytes, execStreamCloseBytes } from "./native-exec-common";
22
+ import { resolveTrustedWindowsTaskkillExe } from "../../lib/windows-elevation";
23
+ export { nativeShellDisabledMessage, type CursorNativeExecPolicyContext } from "./native-exec-policy";
24
+ import { nativeShellDisabledMessage, type CursorNativeExecPolicyContext } from "./native-exec-policy";
22
25
  import {
23
26
  createAdmissionGate,
24
27
  type AdmissionLease,
@@ -32,6 +35,7 @@ export const CURSOR_BACKGROUND_SHELL_TERM_GRACE_MS = 2_000;
32
35
 
33
36
  type BackgroundShellTerminationReason = "session_close" | "idle" | "absolute" | "shutdown";
34
37
  type BackgroundShellTimer = ReturnType<typeof setTimeout>;
38
+ type ProcessGroupLiveness = "alive" | "gone" | "unknown";
35
39
 
36
40
  export interface BackgroundShellTerminationReport {
37
41
  attempted: number;
@@ -42,10 +46,14 @@ export interface BackgroundShellTerminationReport {
42
46
 
43
47
  export interface BackgroundShellRuntime {
44
48
  spawn: typeof spawn;
49
+ platform: NodeJS.Platform;
45
50
  now(): number;
46
51
  setTimer(callback: () => void, delayMs: number): BackgroundShellTimer;
47
52
  clearTimer(timer: BackgroundShellTimer): void;
48
53
  kill(child: ChildProcessWithoutNullStreams, signal?: NodeJS.Signals): boolean;
54
+ killProcessGroup(pid: number, signal?: NodeJS.Signals): boolean;
55
+ isProcessGroupAlive(pid: number): ProcessGroupLiveness;
56
+ killTree(child: ChildProcessWithoutNullStreams): boolean;
49
57
  }
50
58
 
51
59
  interface BackgroundShellEntry {
@@ -65,10 +73,30 @@ interface BackgroundShellEntry {
65
73
 
66
74
  const defaultBackgroundShellRuntime: BackgroundShellRuntime = {
67
75
  spawn,
76
+ platform: process.platform,
68
77
  now: () => Date.now(),
69
78
  setTimer: (callback, delayMs) => setTimeout(callback, delayMs),
70
79
  clearTimer: timer => clearTimeout(timer),
71
80
  kill: (child, signal) => child.kill(signal),
81
+ killProcessGroup: (pid, signal) => {
82
+ process.kill(-pid, signal);
83
+ return true;
84
+ },
85
+ isProcessGroupAlive: pid => {
86
+ try {
87
+ process.kill(-pid, 0);
88
+ return "alive";
89
+ } catch (error) {
90
+ return (error as NodeJS.ErrnoException).code === "ESRCH" ? "gone" : "unknown";
91
+ }
92
+ },
93
+ killTree: child => {
94
+ execFileSync(resolveTrustedWindowsTaskkillExe(), ["/PID", String(child.pid), "/T", "/F"], {
95
+ stdio: "pipe",
96
+ windowsHide: true,
97
+ });
98
+ return true;
99
+ },
72
100
  };
73
101
 
74
102
  let backgroundShellRuntime = defaultBackgroundShellRuntime;
@@ -81,21 +109,7 @@ let absoluteTerminations = 0;
81
109
  let unresolvedKills = 0;
82
110
  let killFailures = 0;
83
111
 
84
- /** Rejection text when Cursor-native shell is denied by policy (issue #604). */
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) {
112
+ function rejectedShellResult(command: string, cwd: string, started: number, opts: CursorNativeExecPolicyContext = {}) {
99
113
  return create(ShellResultSchema, {
100
114
  result: {
101
115
  case: "failure",
@@ -105,7 +119,7 @@ function rejectedShellResult(command: string, cwd: string, started: number) {
105
119
  exitCode: 1,
106
120
  signal: "",
107
121
  stdout: "",
108
- stderr: nativeShellDisabledMessage(),
122
+ stderr: nativeShellDisabledMessage(opts),
109
123
  executionTime: Date.now() - started,
110
124
  aborted: true,
111
125
  }),
@@ -113,10 +127,10 @@ function rejectedShellResult(command: string, cwd: string, started: number) {
113
127
  });
114
128
  }
115
129
 
116
- export function rejectShellExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
130
+ export function rejectShellExecForPolicy(execMsg: ExecServerMessage, opts: CursorNativeExecPolicyContext = {}): Uint8Array {
117
131
  if (execMsg.message.case !== "shellArgs") throw new Error("invalid shell exec");
118
132
  const args = execMsg.message.value;
119
- return execBytes(execMsg, "shellResult", rejectedShellResult(args.command, resolve(args.workingDirectory || process.cwd()), Date.now()));
133
+ return execBytes(execMsg, "shellResult", rejectedShellResult(args.command, resolve(args.workingDirectory || process.cwd()), Date.now(), opts));
120
134
  }
121
135
 
122
136
  export function shellExec(execMsg: ExecServerMessage): Uint8Array {
@@ -154,7 +168,7 @@ export function shellExec(execMsg: ExecServerMessage): Uint8Array {
154
168
  }));
155
169
  }
156
170
 
157
- export function rejectShellStreamExecForPolicy(execMsg: ExecServerMessage): Uint8Array[] {
171
+ export function rejectShellStreamExecForPolicy(execMsg: ExecServerMessage, opts: CursorNativeExecPolicyContext = {}): Uint8Array[] {
158
172
  if (execMsg.message.case !== "shellStreamArgs") throw new Error("invalid shell stream exec");
159
173
  const args = execMsg.message.value;
160
174
  const cwd = resolve(args.workingDirectory || process.cwd());
@@ -164,12 +178,12 @@ export function rejectShellStreamExecForPolicy(execMsg: ExecServerMessage): Uint
164
178
  event: { case: "start", value: create(ShellStreamStartSchema, { sandboxPolicy: args.requestedSandboxPolicy }) },
165
179
  })),
166
180
  execBytes(execMsg, "shellStream", create(ShellStreamSchema, {
167
- event: { case: "stderr", value: create(ShellStreamStderrSchema, { data: nativeShellDisabledMessage() }) },
181
+ event: { case: "stderr", value: create(ShellStreamStderrSchema, { data: nativeShellDisabledMessage(opts) }) },
168
182
  })),
169
183
  execBytes(execMsg, "shellStream", create(ShellStreamSchema, {
170
184
  event: { case: "exit", value: create(ShellStreamExitSchema, { code: 1, cwd, aborted: true }) },
171
185
  })),
172
- execBytes(execMsg, "shellResult", rejectedShellResult(args.command, cwd, started)),
186
+ execBytes(execMsg, "shellResult", rejectedShellResult(args.command, cwd, started, opts)),
173
187
  execStreamCloseBytes(execMsg),
174
188
  ];
175
189
  }
@@ -260,12 +274,12 @@ export async function shellStreamExec(execMsg: ExecServerMessage): Promise<Uint8
260
274
  return replies;
261
275
  }
262
276
 
263
- export function rejectBackgroundShellSpawnExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
277
+ export function rejectBackgroundShellSpawnExecForPolicy(execMsg: ExecServerMessage, opts: CursorNativeExecPolicyContext = {}): Uint8Array {
264
278
  if (execMsg.message.case !== "backgroundShellSpawnArgs") throw new Error("invalid background shell exec");
265
279
  const args = execMsg.message.value;
266
280
  const cwd = resolve(args.workingDirectory || process.cwd());
267
281
  return execBytes(execMsg, "backgroundShellSpawnResult", create(BackgroundShellSpawnResultSchema, {
268
- result: { case: "error", value: create(BackgroundShellSpawnErrorSchema, { command: args.command, workingDirectory: cwd, error: nativeShellDisabledMessage() }) },
282
+ result: { case: "error", value: create(BackgroundShellSpawnErrorSchema, { command: args.command, workingDirectory: cwd, error: nativeShellDisabledMessage(opts) }) },
269
283
  }));
270
284
  }
271
285
 
@@ -355,6 +369,13 @@ function waitForBackgroundShellClose(entry: BackgroundShellEntry): Promise<boole
355
369
  });
356
370
  }
357
371
 
372
+ function waitForBackgroundShellGrace(): Promise<void> {
373
+ return new Promise(resolveWait => {
374
+ const timer = backgroundShellRuntime.setTimer(resolveWait, CURSOR_BACKGROUND_SHELL_TERM_GRACE_MS);
375
+ void timer;
376
+ });
377
+ }
378
+
358
379
  function tryKillBackgroundShell(entry: BackgroundShellEntry, signal?: NodeJS.Signals): boolean {
359
380
  try {
360
381
  return backgroundShellRuntime.kill(entry.child, signal);
@@ -363,6 +384,34 @@ function tryKillBackgroundShell(entry: BackgroundShellEntry, signal?: NodeJS.Sig
363
384
  }
364
385
  }
365
386
 
387
+ function tryKillBackgroundShellProcessGroup(entry: BackgroundShellEntry, signal?: NodeJS.Signals): boolean {
388
+ const pid = entry.child.pid;
389
+ if (typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0) return false;
390
+ try {
391
+ return backgroundShellRuntime.killProcessGroup(pid, signal);
392
+ } catch {
393
+ return false;
394
+ }
395
+ }
396
+
397
+ function tryKillBackgroundShellTree(entry: BackgroundShellEntry): boolean {
398
+ try {
399
+ return backgroundShellRuntime.killTree(entry.child);
400
+ } catch {
401
+ return false;
402
+ }
403
+ }
404
+
405
+ function isBackgroundShellProcessGroupAlive(entry: BackgroundShellEntry): ProcessGroupLiveness {
406
+ const pid = entry.child.pid;
407
+ if (typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0) return "unknown";
408
+ try {
409
+ return backgroundShellRuntime.isProcessGroupAlive(pid);
410
+ } catch {
411
+ return "unknown";
412
+ }
413
+ }
414
+
366
415
  async function terminateBackgroundShell(
367
416
  entry: BackgroundShellEntry,
368
417
  reason: BackgroundShellTerminationReason,
@@ -380,11 +429,43 @@ async function terminateBackgroundShell(
380
429
  try { entry.child.stderr.resume(); } catch { /* keep draining when supported */ }
381
430
 
382
431
  let attemptKillFailures = 0;
383
- if (!tryKillBackgroundShell(entry)) attemptKillFailures += 1;
384
- let closed = await waitForBackgroundShellClose(entry);
432
+ let treeTerminationStarted = false;
433
+ let processGroupTerminationStarted = false;
434
+ let processGroupTerminationAttempted = false;
435
+ let treeTerminationFailed = false;
436
+ if (backgroundShellRuntime.platform === "win32") {
437
+ treeTerminationStarted = tryKillBackgroundShellTree(entry);
438
+ if (!treeTerminationStarted) {
439
+ treeTerminationFailed = true;
440
+ attemptKillFailures += 1;
441
+ if (!tryKillBackgroundShell(entry)) attemptKillFailures += 1;
442
+ }
443
+ } else {
444
+ processGroupTerminationAttempted = true;
445
+ if (tryKillBackgroundShellProcessGroup(entry, "SIGTERM")) {
446
+ processGroupTerminationStarted = true;
447
+ } else {
448
+ attemptKillFailures += 1;
449
+ if (!tryKillBackgroundShell(entry)) attemptKillFailures += 1;
450
+ }
451
+ }
452
+ let closed = processGroupTerminationStarted ? false : await waitForBackgroundShellClose(entry);
385
453
  if (!closed && backgroundShells.get(entry.shellId) !== entry) closed = true;
386
- if (!closed) {
387
- if (!tryKillBackgroundShell(entry, "SIGKILL")) attemptKillFailures += 1;
454
+ if (processGroupTerminationStarted) {
455
+ await waitForBackgroundShellGrace();
456
+ if (isBackgroundShellProcessGroupAlive(entry) !== "gone" && !tryKillBackgroundShellProcessGroup(entry, "SIGKILL")) {
457
+ attemptKillFailures += 1;
458
+ }
459
+ closed = backgroundShells.get(entry.shellId) !== entry;
460
+ }
461
+ if ((!closed || treeTerminationFailed || processGroupTerminationAttempted && !processGroupTerminationStarted)
462
+ && !treeTerminationStarted && !processGroupTerminationStarted) {
463
+ if (backgroundShellRuntime.platform === "win32") {
464
+ if (!tryKillBackgroundShell(entry, "SIGKILL")) attemptKillFailures += 1;
465
+ } else if (!tryKillBackgroundShellProcessGroup(entry, "SIGKILL")) {
466
+ attemptKillFailures += 1;
467
+ if (!closed && !tryKillBackgroundShell(entry, "SIGKILL")) attemptKillFailures += 1;
468
+ }
388
469
  closed = await waitForBackgroundShellClose(entry);
389
470
  if (!closed && backgroundShells.get(entry.shellId) !== entry) closed = true;
390
471
  }
@@ -471,7 +552,11 @@ export function backgroundShellSpawnExec(execMsg: ExecServerMessage, sessionId:
471
552
  if (!admissionLease) return backgroundShellSpawnError(execMsg, args.command, cwd, "background shell limit reached");
472
553
  let child: ChildProcessWithoutNullStreams;
473
554
  try {
474
- child = backgroundShellRuntime.spawn(args.command, { cwd, shell: true });
555
+ child = backgroundShellRuntime.spawn(args.command, {
556
+ cwd,
557
+ shell: true,
558
+ ...(backgroundShellRuntime.platform === "win32" ? {} : { detached: true }),
559
+ });
475
560
  } catch (err) {
476
561
  admissionLease.release();
477
562
  return backgroundShellSpawnError(execMsg, args.command, cwd, errorText(err));
@@ -517,10 +602,10 @@ export function backgroundShellSpawnExec(execMsg: ExecServerMessage, sessionId:
517
602
  }
518
603
  }
519
604
 
520
- export function rejectWriteShellStdinExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
605
+ export function rejectWriteShellStdinExecForPolicy(execMsg: ExecServerMessage, opts: CursorNativeExecPolicyContext = {}): Uint8Array {
521
606
  if (execMsg.message.case !== "writeShellStdinArgs") throw new Error("invalid shell stdin exec");
522
607
  return execBytes(execMsg, "writeShellStdinResult", create(WriteShellStdinResultSchema, {
523
- result: { case: "error", value: create(WriteShellStdinErrorSchema, { error: nativeShellDisabledMessage() }) },
608
+ result: { case: "error", value: create(WriteShellStdinErrorSchema, { error: nativeShellDisabledMessage(opts) }) },
524
609
  }));
525
610
  }
526
611
 
@@ -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
@@ -403,6 +410,19 @@ export function storeCursorBlob(data: Uint8Array, requestScope?: CursorBlobReque
403
410
  return blobId;
404
411
  }
405
412
 
413
+ /**
414
+ * Serve-time integrity for content-addressed blobs (devlog 260826_cursor_responses_gap 080):
415
+ * a raw 32-byte blob id IS the SHA-256 of its bytes, so served data whose digest mismatches
416
+ * the id means in-store corruption — the splice signature behind garbled replayed tool
417
+ * results. Ids longer than 32 bytes (digested-key namespace) and server-minted ids are not
418
+ * content-addressed and always pass.
419
+ */
420
+ export function cursorBlobServeIntegrityOk(blobId: Uint8Array, served: Uint8Array): boolean {
421
+ if (blobId.byteLength !== 32) return true;
422
+ const digest = createHash("sha256").update(served).digest();
423
+ return digest.equals(Buffer.from(blobId));
424
+ }
425
+
406
426
  /**
407
427
  * Long-lived pin for blobs referenced by an active Cursor conversation checkpoint.
408
428
  * Unlike a request scope, this lease is not sealed and is not released by getBlob hydration.
@@ -556,16 +576,17 @@ export async function handleCursorNativeExec(execMsg: ExecServerMessage, deps: C
556
576
  }))];
557
577
  }
558
578
  if (!cursorUnsafeNativeLocalExecEnabled(deps)) {
559
- if (execCase === "readArgs") return [rejectReadExecForPolicy(execMsg)];
560
- if (execCase === "writeArgs") return [rejectWriteExecForPolicy(execMsg)];
561
- if (execCase === "deleteArgs") return [rejectDeleteExecForPolicy(execMsg)];
562
- if (execCase === "lsArgs") return [rejectLsExecForPolicy(execMsg)];
563
- if (execCase === "grepArgs") return [rejectGrepExecForPolicy(execMsg)];
564
- if (execCase === "shellArgs") return [rejectShellExecForPolicy(execMsg)];
565
- if (execCase === "shellStreamArgs") return rejectShellStreamExecForPolicy(execMsg);
566
- if (execCase === "backgroundShellSpawnArgs") return [rejectBackgroundShellSpawnExecForPolicy(execMsg)];
567
- if (execCase === "writeShellStdinArgs") return [rejectWriteShellStdinExecForPolicy(execMsg)];
568
- if (execCase === "fetchArgs") return [rejectFetchExecForPolicy(execMsg)];
579
+ const policy = nativeExecPolicyContext(deps);
580
+ if (execCase === "readArgs") return [rejectReadExecForPolicy(execMsg, policy)];
581
+ if (execCase === "writeArgs") return [rejectWriteExecForPolicy(execMsg, policy)];
582
+ if (execCase === "deleteArgs") return [rejectDeleteExecForPolicy(execMsg, policy)];
583
+ if (execCase === "lsArgs") return [rejectLsExecForPolicy(execMsg, policy)];
584
+ if (execCase === "grepArgs") return [rejectGrepExecForPolicy(execMsg, policy)];
585
+ if (execCase === "shellArgs") return [rejectShellExecForPolicy(execMsg, policy)];
586
+ if (execCase === "shellStreamArgs") return rejectShellStreamExecForPolicy(execMsg, policy);
587
+ if (execCase === "backgroundShellSpawnArgs") return [rejectBackgroundShellSpawnExecForPolicy(execMsg, policy)];
588
+ if (execCase === "writeShellStdinArgs") return [rejectWriteShellStdinExecForPolicy(execMsg, policy)];
589
+ if (execCase === "fetchArgs") return [rejectFetchExecForPolicy(execMsg, policy)];
569
590
  }
570
591
  if (execCase === "readArgs") return [readExec(execMsg)];
571
592
  if (execCase === "writeArgs") return [deps.rejectNativeFileMutations ? rejectWriteExecForApplyPatch(execMsg, deps.structuredEditAvailable === true) : writeExec(execMsg)];
@@ -622,6 +643,13 @@ export function handleCursorNativeKv(
622
643
  if (kvMsg.message.case === "getBlobArgs") {
623
644
  const blobKey = key(kvMsg.message.value.blobId);
624
645
  const blobData = getBlob(blobKey);
646
+ // Splice-class corruption guard (devlog 260826 080): diagnostic only, never blocks serving.
647
+ if (blobData && !cursorBlobServeIntegrityOk(kvMsg.message.value.blobId, blobData)) {
648
+ debugProviderDiagnostic("cursor", "blob-integrity-mismatch", {
649
+ blobKey: blobKey.slice(0, 18),
650
+ servedBytes: blobData.byteLength,
651
+ });
652
+ }
625
653
  if (blobData && requestScope && blobRequestScopes.get(requestScope)?.kind === "request") {
626
654
  releaseHydratedBlob(blobKey, requestScope);
627
655
  }
@@ -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 [];