@bitkyc08/opencodex 2.81.0 → 2.83.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 (272) hide show
  1. package/bin/ocx.mjs +166 -73
  2. package/gui/dist/assets/App-Ba8uG696.js +54 -0
  3. package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-IImbNPRy.js} +1 -1
  4. package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
  5. package/gui/dist/assets/{index-RsoaAxXY.js → index-BHRZdJuv.js} +13 -13
  6. package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DiJO6sYY.js} +1 -1
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/anthropic.ts +1 -1
  10. package/src/adapters/cursor/catalog.ts +2 -2
  11. package/src/adapters/cursor/effort-map.ts +1 -1
  12. package/src/adapters/devin/live-models.ts +2 -2
  13. package/src/adapters/google-truncation.ts +22 -0
  14. package/src/adapters/google.ts +7 -3
  15. package/src/adapters/ollama-native.ts +18 -14
  16. package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
  17. package/src/adapters/openai-responses/passthrough.ts +11 -4
  18. package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
  19. package/src/adapters/openai-responses/reasoning.ts +11 -1
  20. package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
  21. package/src/bridge/errors.ts +7 -1
  22. package/src/claude/agents-inject.ts +14 -3
  23. package/src/claude/context-windows.ts +22 -20
  24. package/src/claude/desktop-3p.ts +88 -12
  25. package/src/claude/desktop-picker.ts +1 -1
  26. package/src/claude/desktop-profile.ts +2 -1
  27. package/src/claude/gateway-cache.ts +18 -1
  28. package/src/claude/inbound.ts +77 -3
  29. package/src/claude/intercept/cli-catalog.ts +3 -0
  30. package/src/claude/intercept/picker-ca-persistence.ts +7 -6
  31. package/src/claude/intercept/picker-models.ts +14 -6
  32. package/src/claude/intercept/settings.ts +3 -0
  33. package/src/claude/long-context.ts +66 -0
  34. package/src/claude/model-info.ts +19 -16
  35. package/src/claude/outbound.ts +94 -9
  36. package/src/claude/subagent-model.ts +40 -11
  37. package/src/cli/account-auth.ts +20 -12
  38. package/src/cli/agent.ts +9 -5
  39. package/src/cli/aside-profile-recovery.ts +33 -0
  40. package/src/cli/capabilities-base.ts +5 -5
  41. package/src/cli/capabilities-integrations.ts +2 -2
  42. package/src/cli/cli-path-diagnostics.ts +132 -0
  43. package/src/cli/desktop-runtime-guidance.ts +65 -0
  44. package/src/cli/dispatch.ts +5 -0
  45. package/src/cli/doctor.ts +16 -3
  46. package/src/cli/index.ts +14 -9
  47. package/src/cli/integration-aside-sync.ts +2 -1
  48. package/src/cli/integrations.ts +16 -3
  49. package/src/cli/resolve.ts +26 -3
  50. package/src/cli/root.ts +4 -2
  51. package/src/cli/status.ts +64 -12
  52. package/src/cli/update-restart-child.ts +4 -2
  53. package/src/cli/update-restart-home.ts +28 -10
  54. package/src/cli/update-restart-service-record.ts +116 -0
  55. package/src/cli/update-restart-supervision.ts +96 -0
  56. package/src/cli/update-restart.ts +59 -13
  57. package/src/cli/v2.ts +27 -20
  58. package/src/cli/version-skew-notice.ts +77 -0
  59. package/src/clients/config-export/model-metadata.ts +16 -0
  60. package/src/clients/config-export/zcode-store.ts +46 -11
  61. package/src/clients/config-export/zcode.ts +3 -5
  62. package/src/clients/config-export.ts +1 -1
  63. package/src/clients/lazycodex.ts +1 -1
  64. package/src/clients/omo-role-models.ts +32 -7
  65. package/src/codex/auth-api/account-selection.ts +107 -0
  66. package/src/codex/auth-api/pool-mode-gate.ts +4 -5
  67. package/src/codex/auth-api/routes.ts +10 -49
  68. package/src/codex/auth-context.ts +28 -4
  69. package/src/codex/autostart-health.ts +12 -3
  70. package/src/codex/catalog/build-entries.ts +18 -0
  71. package/src/codex/catalog/bundled.ts +155 -26
  72. package/src/codex/catalog/combo-member.ts +3 -2
  73. package/src/codex/catalog/effort.ts +26 -5
  74. package/src/codex/catalog/gather-capture.ts +5 -0
  75. package/src/codex/catalog/model-hints.ts +15 -8
  76. package/src/codex/catalog/model-visibility.ts +3 -1
  77. package/src/codex/catalog/native-models.ts +1 -1
  78. package/src/codex/catalog/operator-tiers.ts +38 -0
  79. package/src/codex/catalog/provider-models.ts +68 -35
  80. package/src/codex/catalog/retained-sync.ts +7 -5
  81. package/src/codex/catalog/routed-gather.ts +15 -1
  82. package/src/codex/catalog-auto-refresh-sources.ts +3 -4
  83. package/src/codex/catalog-auto-refresh.ts +19 -0
  84. package/src/codex/catalog.ts +1 -1
  85. package/src/codex/codex-home-owner.ts +3 -0
  86. package/src/codex/config-write-lock.ts +333 -0
  87. package/src/codex/convergence.ts +2 -1
  88. package/src/codex/desktop-app/darwin.ts +1 -1
  89. package/src/codex/desktop-app/handoff.ts +1 -1
  90. package/src/codex/desktop-app/linux.ts +1 -1
  91. package/src/codex/desktop-app/lock.ts +1 -1
  92. package/src/codex/desktop-app/types.ts +1 -1
  93. package/src/codex/desktop-app/windows.ts +1 -1
  94. package/src/codex/desktop-app-restart.ts +1 -1
  95. package/src/codex/features.ts +257 -200
  96. package/src/codex/inject/config-write-section.ts +34 -0
  97. package/src/codex/inject/multi-agent-v2.ts +14 -6
  98. package/src/codex/inject/remove.ts +36 -11
  99. package/src/codex/inject/restore.ts +85 -20
  100. package/src/codex/inject-coordination.ts +15 -6
  101. package/src/codex/inject.ts +62 -20
  102. package/src/codex/journal.ts +62 -17
  103. package/src/codex/main-account-cache.ts +43 -0
  104. package/src/codex/main-account.ts +6 -7
  105. package/src/codex/paths.ts +2 -0
  106. package/src/codex/prepared-home.ts +18 -0
  107. package/src/codex/prompt-journal.ts +25 -12
  108. package/src/codex/prompt-layers.ts +49 -26
  109. package/src/codex/prompt-lock-claim.ts +134 -0
  110. package/src/codex/prompt-lock-evidence.ts +26 -0
  111. package/src/codex/prompt-lock-io.ts +16 -0
  112. package/src/codex/prompt-lock-owner.ts +130 -0
  113. package/src/codex/prompt-lock.ts +84 -53
  114. package/src/codex/routing/failure-window.ts +146 -0
  115. package/src/codex/routing/selection.ts +4 -1
  116. package/src/codex/routing.ts +13 -17
  117. package/src/codex/runtime.ts +106 -18
  118. package/src/codex/shim.ts +45 -19
  119. package/src/combos/jev-service-exchange.ts +281 -0
  120. package/src/combos/jev.ts +34 -220
  121. package/src/config/atomic-write.ts +1 -1
  122. package/src/config/codex-ws-reuse-setting.ts +10 -0
  123. package/src/config/live-reconcile.ts +17 -0
  124. package/src/config/load-degrade.ts +8 -0
  125. package/src/config/rebase-provenance.ts +11 -0
  126. package/src/config/schema/config-schema.ts +4 -0
  127. package/src/generated/compatibility-version.json +375 -227
  128. package/src/generated/model-metadata.ts +2 -2
  129. package/src/grok/inject.ts +276 -206
  130. package/src/grok/reset-coupon-ledger.ts +223 -57
  131. package/src/grok/reset-coupons.ts +13 -6
  132. package/src/grok/sync.ts +2 -1
  133. package/src/images/loop.ts +29 -11
  134. package/src/integrations/aside-profiles.ts +10 -3
  135. package/src/integrations/catalog-refresh.ts +18 -2
  136. package/src/integrations/cursor-detect.ts +1 -1
  137. package/src/integrations/writer.ts +9 -3
  138. package/src/lib/bun-path-runtime.d.mts +32 -0
  139. package/src/lib/bun-path-runtime.mjs +95 -0
  140. package/src/lib/bun-runtime-preflight.ts +140 -0
  141. package/src/lib/desktop-cli-handoff.d.mts +20 -0
  142. package/src/lib/desktop-cli-handoff.mjs +92 -0
  143. package/src/lib/desktop-cli-record.d.mts +29 -0
  144. package/src/lib/desktop-cli-record.mjs +102 -0
  145. package/src/lib/errors.ts +25 -0
  146. package/src/lib/redact.ts +2 -2
  147. package/src/lib/request-execution-budget.ts +8 -0
  148. package/src/lib/synced-state-location.ts +46 -6
  149. package/src/lib/test-home-guard.ts +87 -1
  150. package/src/lib/translator-budget.ts +17 -4
  151. package/src/lib/windows-atomic-replace.ts +38 -16
  152. package/src/lib/windows-elevation.ts +6 -0
  153. package/src/lib/windows-secret-acl.ts +92 -25
  154. package/src/lib/winsw.ts +2 -3
  155. package/src/oauth/anthropic-account-refusal.ts +39 -1
  156. package/src/oauth/anthropic-routing.ts +1 -1
  157. package/src/oauth/anthropic-send-ownership.ts +3 -3
  158. package/src/oauth/chatgpt.ts +113 -41
  159. package/src/oauth/index.ts +7 -3
  160. package/src/oauth/meta-muse-device.ts +1 -1
  161. package/src/oauth/store.ts +1 -1
  162. package/src/oauth/types.ts +1 -1
  163. package/src/providers/anthropic-fast.ts +1 -1
  164. package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
  165. package/src/providers/azure-model-metadata.ts +115 -0
  166. package/src/providers/derive.ts +21 -0
  167. package/src/providers/devin-cli-authmode-migration.ts +1 -1
  168. package/src/providers/devin-provider-merge-migration.ts +1 -1
  169. package/src/providers/github-copilot-auto.ts +169 -0
  170. package/src/providers/model-presets.ts +1 -1
  171. package/src/providers/muse-subscription-usage.ts +1 -1
  172. package/src/providers/quota/vendor-probes-key.ts +127 -17
  173. package/src/providers/quota.ts +1 -1
  174. package/src/providers/registry/entries-core.ts +6 -6
  175. package/src/providers/registry/entries-extended.ts +4 -0
  176. package/src/providers/registry/model-seeds.ts +3 -3
  177. package/src/providers/xai-fast-model.ts +1 -1
  178. package/src/responses/reasoning-envelope.ts +29 -1
  179. package/src/responses/reasoning-replay-cache.ts +17 -1
  180. package/src/responses/spill-store.ts +15 -2
  181. package/src/responses/state/metrics.ts +8 -0
  182. package/src/responses/state/spill-failure.ts +12 -0
  183. package/src/responses/state/spill-queue.ts +12 -5
  184. package/src/responses/state.ts +18 -10
  185. package/src/responses/task-input.ts +17 -0
  186. package/src/server/background-lifecycle.ts +27 -0
  187. package/src/server/chat-completions.ts +1 -0
  188. package/src/server/chat-native.ts +28 -12
  189. package/src/server/claude-messages.ts +43 -6
  190. package/src/server/inbound-body-admission.ts +25 -7
  191. package/src/server/index/claude-intercept-lifecycle.ts +3 -1
  192. package/src/server/index/link-listener.ts +30 -17
  193. package/src/server/index/serve-options.ts +18 -4
  194. package/src/server/index/spend-ledger-lifecycle.ts +2 -1
  195. package/src/server/index/startup-warnings.ts +19 -0
  196. package/src/server/index.ts +2 -1
  197. package/src/server/lifecycle.ts +31 -4
  198. package/src/server/management/agent-settings-routes.ts +93 -32
  199. package/src/server/management/codex-agent-role-routes.ts +4 -2
  200. package/src/server/management/codex-prompt-routes.ts +1 -0
  201. package/src/server/management/codex-role-auto-assign.ts +13 -2
  202. package/src/server/management/config-routes.ts +59 -8
  203. package/src/server/management/context.ts +2 -2
  204. package/src/server/management/grok-coupon-routes.ts +277 -52
  205. package/src/server/management/link-routes.ts +42 -20
  206. package/src/server/management/route-registry.ts +2 -2
  207. package/src/server/management/shared.ts +3 -3
  208. package/src/server/messages-native-oauth.ts +9 -8
  209. package/src/server/messages-native.ts +49 -13
  210. package/src/server/messages-response-headers.ts +14 -0
  211. package/src/server/relay-eager.ts +2 -0
  212. package/src/server/relay.ts +18 -7
  213. package/src/server/request-log-conversation.ts +48 -5
  214. package/src/server/request-log-failure-attribution.ts +3 -0
  215. package/src/server/request-log-terminal-status.ts +186 -0
  216. package/src/server/request-log.ts +15 -89
  217. package/src/server/responses/adapter-continuation.ts +11 -3
  218. package/src/server/responses/adapter-dispatch.ts +99 -19
  219. package/src/server/responses/codex-ws-exchange.ts +17 -2
  220. package/src/server/responses/codex-ws-pool.ts +100 -13
  221. package/src/server/responses/codex-ws-wire.ts +9 -0
  222. package/src/server/responses/combo-requested-effort.ts +3 -1
  223. package/src/server/responses/compact.ts +3 -2
  224. package/src/server/responses/core-codex-account.ts +22 -1
  225. package/src/server/responses/core-combo-native.ts +20 -3
  226. package/src/server/responses/core-combo.ts +22 -14
  227. package/src/server/responses/core-lifetime.ts +3 -36
  228. package/src/server/responses/core-opaque-recovery.ts +5 -5
  229. package/src/server/responses/core-options.ts +5 -0
  230. package/src/server/responses/core-replay.ts +73 -0
  231. package/src/server/responses/core.ts +9 -9
  232. package/src/server/responses/empty-completion-guard.ts +18 -2
  233. package/src/server/responses/fetch-helpers.ts +26 -3
  234. package/src/server/responses/jev-model-invoke.ts +1 -1
  235. package/src/server/responses/passthrough-delivery.ts +24 -2
  236. package/src/server/responses/passthrough-dispatch.ts +134 -62
  237. package/src/server/responses/passthrough-execution.ts +3 -3
  238. package/src/server/responses/request-prepare.ts +4 -0
  239. package/src/server/responses/request-send-budget.ts +112 -49
  240. package/src/server/responses/request-transport.ts +125 -5
  241. package/src/server/responses/response-effects.ts +1 -0
  242. package/src/server/responses/run-turn-execution.ts +15 -5
  243. package/src/server/responses/sidecar-execution.ts +30 -16
  244. package/src/server/responses/sidecar-send-budget.ts +58 -0
  245. package/src/server/responses/spend-storage-error.ts +33 -0
  246. package/src/server/responses/ws-upstream.ts +19 -6
  247. package/src/server/startup-health-cache.ts +2 -1
  248. package/src/server/workflow-refusal.ts +3 -0
  249. package/src/service/cli.ts +20 -4
  250. package/src/service/desktop-command-guard.ts +32 -0
  251. package/src/service/desktop-startup.ts +66 -40
  252. package/src/service/desktop-supervision.d.mts +26 -0
  253. package/src/service/desktop-supervision.mjs +140 -0
  254. package/src/service/managing-cli.ts +1 -0
  255. package/src/service/orchestration.ts +46 -20
  256. package/src/service/repair.ts +25 -9
  257. package/src/service/state.ts +4 -3
  258. package/src/service/systemd.ts +17 -8
  259. package/src/service/windows-ops.ts +61 -22
  260. package/src/storage/policy-job.ts +5 -1
  261. package/src/storage/policy-worker.ts +3 -1
  262. package/src/storage/policy.ts +9 -0
  263. package/src/types/config.ts +21 -0
  264. package/src/types/request.ts +10 -0
  265. package/src/types/wire.ts +1 -0
  266. package/src/update/index.ts +57 -9
  267. package/src/update/restart-ownership.ts +10 -3
  268. package/src/update/runtime-ownership.d.mts +6 -0
  269. package/src/update/runtime-ownership.mjs +23 -7
  270. package/src/usage/expected-prices.ts +1 -1
  271. package/src/web-search/loop.ts +5 -2
  272. package/gui/dist/assets/App-7yhoys8a.js +0 -54
@@ -801,6 +801,11 @@ export interface CoordinatedIntegrationOptions {
801
801
  * non-null result refuses without writing anything.
802
802
  */
803
803
  revalidate?: (frozen: IntegrationWriteInput) => Promise<WriteOutcome | null>;
804
+ /**
805
+ * Synchronous last word, evaluated with no await between it and the transaction (after
806
+ * `revalidate` and any lock acquisition). A non-null result refuses without writing.
807
+ */
808
+ guard?: (frozen: IntegrationWriteInput) => WriteOutcome | null;
804
809
  }
805
810
 
806
811
  /** Freeze all mutable resolution seams before the first lock await. */
@@ -942,21 +947,22 @@ async function coordinatedWrite(
942
947
  if (!prepared.ok) return prepared.refusal;
943
948
  const frozen = prepared.value;
944
949
  const spec = INTEGRATION_CLIENTS[frozen.clientId];
950
+ const guarded = (bound: IntegrationWriteInput) => options?.guard?.(bound) ?? operation(bound);
945
951
  if (!spec.writerLock) {
946
952
  const refused = await options?.revalidate?.(frozen);
947
- return refused ?? operation(frozen);
953
+ return refused ?? guarded(frozen);
948
954
  }
949
955
 
950
956
  // An absent client home is not created merely to acquire a sibling lock.
951
957
  if (frozen.io.statKind(frozen.resolvedPaths.detectDir) !== "dir") {
952
958
  const refused = await options?.revalidate?.(frozen);
953
959
  if (refused) return refused;
954
- if (frozen.io.statKind(frozen.resolvedPaths.detectDir) !== "dir") return operation(frozen);
960
+ if (frozen.io.statKind(frozen.resolvedPaths.detectDir) !== "dir") return guarded(frozen);
955
961
  }
956
962
  return withClientLocks(
957
963
  frozen,
958
964
  spec.writerLock.suffix,
959
- () => operation(frozen),
965
+ () => guarded(frozen),
960
966
  options,
961
967
  );
962
968
  }
@@ -0,0 +1,32 @@
1
+ export interface DesktopCliIo {
2
+ stat?: (path: string) => { isFile(): boolean };
3
+ access?: (path: string, mode: number) => void;
4
+ }
5
+ export interface PathBunIo extends DesktopCliIo {
6
+ now?: () => number;
7
+ realpath?: (path: string) => string;
8
+ stat?: (path: string) => { isFile(): boolean; mode: number };
9
+ isRealBunBinary?: (path: string) => boolean;
10
+ spawnSync?: (path: string, args: string[], options: {
11
+ env: NodeJS.ProcessEnv;
12
+ encoding: "utf8";
13
+ timeout: number;
14
+ maxBuffer: number;
15
+ killSignal: "SIGKILL";
16
+ shell: false;
17
+ windowsHide: true;
18
+ stdio: ["ignore", "pipe", "pipe"];
19
+ }) => { status: number | null; stdout?: string; signal?: string | null; error?: unknown };
20
+ }
21
+ export declare function findPathBun(options: {
22
+ env?: NodeJS.ProcessEnv;
23
+ platform?: NodeJS.Platform;
24
+ pinnedVersion: string;
25
+ deadlineMs?: number;
26
+ io?: PathBunIo;
27
+ }): { path: string; version: string } | null;
28
+ export declare function findDesktopCli(options?: {
29
+ platform?: NodeJS.Platform;
30
+ home?: string;
31
+ io?: DesktopCliIo;
32
+ }): string | null;
@@ -0,0 +1,95 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { accessSync, constants, realpathSync, statSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { posix, win32 } from "node:path";
5
+ import { performance } from "node:perf_hooks";
6
+ import { isRealBunBinary } from "./bun-binary-validator.mjs";
7
+
8
+ const STABLE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
9
+ const MAX_VERSION_BYTES = 4096;
10
+ const IDENTITY_PROBE = "process.stdout.write(String(typeof Bun==='object'&&Bun.version))";
11
+
12
+ function stableVersion(value) {
13
+ if (typeof value !== "string" || value.length > 64) return null;
14
+ const match = STABLE.exec(value);
15
+ if (!match) return null;
16
+ const numbers = match.slice(1, 4).map(Number);
17
+ return numbers.every(Number.isSafeInteger) ? numbers : null;
18
+ }
19
+
20
+ /** Discover the first absolute PATH Bun passing version-policy and identity checks. */
21
+ export function findPathBun({
22
+ env = process.env, platform = process.platform, pinnedVersion,
23
+ deadlineMs = 750, io = {},
24
+ }) {
25
+ const pin = stableVersion(pinnedVersion);
26
+ if (!pin || !Number.isFinite(deadlineMs) || deadlineMs <= 0) return null;
27
+ const paths = platform === "win32" ? win32 : posix;
28
+ const absolute = value => paths.isAbsolute(value)
29
+ && (platform !== "win32" || /^(?:[A-Za-z]:[\\/]|\\\\[^\\/]+[\\/][^\\/]+)/.test(value));
30
+ const now = io.now ?? (() => performance.now());
31
+ const end = now() + deadlineMs;
32
+ const realpath = io.realpath ?? realpathSync.native;
33
+ const stat = io.stat ?? statSync;
34
+ const access = io.access ?? accessSync;
35
+ const validate = io.isRealBunBinary ?? isRealBunBinary;
36
+ const spawn = io.spawnSync ?? spawnSync;
37
+ const key = platform === "win32"
38
+ ? Object.keys(env).find(name => name.toLowerCase() === "path") : "PATH";
39
+ const directories = (env[key ?? "PATH"] ?? "").split(platform === "win32" ? ";" : ":");
40
+ const seen = new Set();
41
+ const probe = (path, args) => {
42
+ const remaining = Math.floor(end - now());
43
+ if (remaining <= 0) return null;
44
+ const result = spawn(path, args, {
45
+ env, encoding: "utf8", timeout: remaining, maxBuffer: MAX_VERSION_BYTES,
46
+ killSignal: "SIGKILL", shell: false, windowsHide: true,
47
+ stdio: ["ignore", "pipe", "pipe"],
48
+ });
49
+ if (now() >= end || result.error || result.signal || result.status !== 0
50
+ || typeof result.stdout !== "string" || result.stdout.length > MAX_VERSION_BYTES) return null;
51
+ return result.stdout.trim();
52
+ };
53
+ for (const directory of directories) {
54
+ if (now() >= end) break;
55
+ if (!directory || !absolute(directory)) continue;
56
+ try {
57
+ const path = realpath(paths.join(directory, platform === "win32" ? "bun.exe" : "bun"));
58
+ if (!absolute(path)) continue;
59
+ const identity = platform === "win32" ? path.toLowerCase() : path;
60
+ if (seen.has(identity)) continue;
61
+ seen.add(identity);
62
+ const file = stat(path);
63
+ if (!file.isFile()) continue;
64
+ if (platform !== "win32" && ((file.mode & 0o022) || (stat(paths.dirname(path)).mode & 0o022))) continue;
65
+ access(path, constants.X_OK);
66
+ if (!validate(path)) continue;
67
+ const version = probe(path, ["--version"]);
68
+ const candidate = stableVersion(version);
69
+ if (!candidate || candidate[0] !== pin[0] || candidate[1] < pin[1]) continue;
70
+ if (probe(path, ["-e", IDENTITY_PROBE]) !== version) continue;
71
+ return { path, version };
72
+ } catch {
73
+ // Unreadable, unexecutable, vanished or invalid: try the next absolute candidate.
74
+ }
75
+ }
76
+ return null;
77
+ }
78
+
79
+ /** Failure-text pointer only. Never execute or delegate to this binary. */
80
+ export function findDesktopCli({ platform = process.platform, home = homedir(), io = {} } = {}) {
81
+ if (platform !== "darwin") return null;
82
+ const stat = io.stat ?? statSync;
83
+ const access = io.access ?? accessSync;
84
+ for (const path of [
85
+ "/Applications/OpenCodex.app/Contents/MacOS/ocx",
86
+ posix.join(home, "Applications/OpenCodex.app/Contents/MacOS/ocx"),
87
+ ]) {
88
+ try {
89
+ if (!stat(path).isFile()) continue;
90
+ access(path, constants.X_OK);
91
+ return path;
92
+ } catch { /* Missing or not executable: no pointer for this candidate. */ }
93
+ }
94
+ return null;
95
+ }
@@ -0,0 +1,140 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { lstatSync, mkdirSync, readdirSync, rmdirSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import type { DurableBunRuntime } from "./bun-runtime";
6
+
7
+ export type RuntimePreflightReason = "spawn" | "timeout" | "create" | "remove" | "protocol";
8
+ export const RUNTIME_PREFLIGHT_TIMEOUT_MS = 5_000;
9
+ export const RUNTIME_PREFLIGHT_GUIDANCE = "The selected Bun runtime could not create and remove a directory in the OpenCodex config directory. "
10
+ + "Windows application policy may deny writes from this executable location. "
11
+ + "Set OPENCODEX_BUN_PATH to a trusted Bun executable allowed by your policy before launching ocx, "
12
+ + "or reinstall opencodex with npm install -g @bitkyc08/opencodex, then retry. No alternate runtime was selected.";
13
+
14
+ export class RuntimePreflightError extends Error {
15
+ readonly code = "OCX_RUNTIME_PREFLIGHT_FAILED";
16
+ constructor(readonly reason: RuntimePreflightReason) {
17
+ super(`${RUNTIME_PREFLIGHT_GUIDANCE} (preflight: ${reason})`);
18
+ this.name = "RuntimePreflightError";
19
+ }
20
+ }
21
+
22
+ type DirectoryIdentity = { dev: number; ino: number; isDirectory(): boolean; isSymbolicLink(): boolean };
23
+ export interface RuntimePreflightFs {
24
+ lstatSync: (path: string) => DirectoryIdentity;
25
+ mkdirSync: (path: string, mode: number) => unknown;
26
+ rmdirSync: (path: string) => void;
27
+ readdirSync: (path: string) => readonly string[];
28
+ }
29
+ export interface RuntimePreflightOptions {
30
+ platform?: NodeJS.Platform;
31
+ rootWasAbsent?: boolean;
32
+ spawnSync?: typeof spawnSync;
33
+ fs?: RuntimePreflightFs;
34
+ nonce?: () => string;
35
+ }
36
+ /** Caller seams keep one selection through admission, rendering and state publication. */
37
+ export interface RuntimePreflightDeps {
38
+ selectRuntime?: () => DurableBunRuntime;
39
+ assertRuntimeWritable?: typeof assertSelectedRuntimeWritable;
40
+ configDir?: () => string;
41
+ platform?: NodeJS.Platform;
42
+ }
43
+
44
+ const probeFs: RuntimePreflightFs = { lstatSync, mkdirSync, rmdirSync, readdirSync };
45
+
46
+ function sameDirectory(fs: RuntimePreflightFs, path: string, owned: DirectoryIdentity): boolean {
47
+ const current = fs.lstatSync(path);
48
+ return current.isDirectory() && !current.isSymbolicLink()
49
+ && current.dev === owned.dev && current.ino === owned.ino;
50
+ }
51
+
52
+ function removeOwnedEmptyDirectory(fs: RuntimePreflightFs, path: string, owned: DirectoryIdentity): void {
53
+ try {
54
+ if (sameDirectory(fs, path, owned) && fs.readdirSync(path).length === 0
55
+ && sameDirectory(fs, path, owned)) fs.rmdirSync(path);
56
+ } catch { /* Never delete contents or a replacement; cleanup does not mask refusal. */ }
57
+ }
58
+
59
+ function prepareRoot(fs: RuntimePreflightFs, root: string, rootWasAbsent?: boolean): void {
60
+ try {
61
+ if (rootWasAbsent === undefined) {
62
+ try { fs.lstatSync(root); rootWasAbsent = false; }
63
+ catch (error) {
64
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
65
+ rootWasAbsent = true;
66
+ }
67
+ }
68
+ if (rootWasAbsent) {
69
+ try {
70
+ fs.mkdirSync(root, 0o700); // Exclusive, non-recursive: a missing parent refuses.
71
+ } catch (error) {
72
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
73
+ // Use a concurrently created root, subject to the directory check below.
74
+ }
75
+ }
76
+ const current = fs.lstatSync(root);
77
+ if (!current.isDirectory() || current.isSymbolicLink()) throw new RuntimePreflightError("create");
78
+ } catch {
79
+ throw new RuntimePreflightError("create");
80
+ }
81
+ }
82
+
83
+ function probeInProcess(fs: RuntimePreflightFs, path: string): void {
84
+ let owned: DirectoryIdentity | undefined;
85
+ try {
86
+ try { fs.mkdirSync(path, 0o700); owned = fs.lstatSync(path); }
87
+ catch { throw new RuntimePreflightError("create"); }
88
+ try { fs.rmdirSync(path); }
89
+ catch { throw new RuntimePreflightError("remove"); }
90
+ } finally {
91
+ if (owned) removeOwnedEmptyDirectory(fs, path, owned);
92
+ }
93
+ }
94
+
95
+ function probeChild(runtime: DurableBunRuntime, path: string, nonce: string, spawn: typeof spawnSync): void {
96
+ // The child emits only this invocation's nonce and a fixed result, never paths/errors.
97
+ const script = `const fs = require("node:fs"); const path = ${JSON.stringify(path)};
98
+ let result = "create", owned;
99
+ try { fs.mkdirSync(path, 0o700); owned = fs.lstatSync(path); result = "remove";
100
+ fs.rmdirSync(path); result = "ok";
101
+ } catch { /* result already names the failed step; details never leave the child */ } finally {
102
+ if (owned) { try { const now = fs.lstatSync(path);
103
+ if (now.isDirectory() && !now.isSymbolicLink() && now.dev === owned.dev && now.ino === owned.ino
104
+ && fs.readdirSync(path).length === 0) fs.rmdirSync(path);
105
+ } catch { /* leave any probe entry that cannot be proven ours */ } }
106
+ }
107
+ process.stdout.write(${JSON.stringify(nonce)} + ":" + result);
108
+ process.exitCode = result === "ok" ? 0 : 1;`;
109
+ let child: ReturnType<typeof spawnSync>;
110
+ try {
111
+ child = spawn(runtime.path, ["-e", script], {
112
+ shell: false, windowsHide: true, timeout: RUNTIME_PREFLIGHT_TIMEOUT_MS,
113
+ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 1024,
114
+ });
115
+ } catch { throw new RuntimePreflightError("spawn"); }
116
+ if (child.error) {
117
+ throw new RuntimePreflightError((child.error as NodeJS.ErrnoException).code === "ETIMEDOUT" ? "timeout" : "spawn");
118
+ }
119
+ const ack = String(child.stdout ?? "");
120
+ if (ack === `${nonce}:create` && child.status === 1) throw new RuntimePreflightError("create");
121
+ if (ack === `${nonce}:remove` && child.status === 1) throw new RuntimePreflightError("remove");
122
+ if (ack !== `${nonce}:ok` || child.status !== 0) throw new RuntimePreflightError("protocol");
123
+ }
124
+
125
+ /** Windows admission of ONLY the already selected runtime. No discovery or memoization. */
126
+ export function assertSelectedRuntimeWritable(
127
+ runtime: DurableBunRuntime,
128
+ configDir: string,
129
+ deps: RuntimePreflightOptions = {},
130
+ ): void {
131
+ if ((deps.platform ?? process.platform) !== "win32") return;
132
+ if (runtime.source === "standalone" && runtime.path !== process.execPath) throw new RuntimePreflightError("protocol");
133
+ const fs = deps.fs ?? probeFs;
134
+ // The config root is durable: never delete it by pathname, even on refusal.
135
+ prepareRoot(fs, configDir, deps.rootWasAbsent);
136
+ const nonce = (deps.nonce ?? randomUUID)();
137
+ const path = join(configDir, `.ocx-runtime-probe-${nonce}`);
138
+ if (runtime.source === "standalone") probeInProcess(fs, path);
139
+ else probeChild(runtime, path, nonce, deps.spawnSync ?? spawnSync);
140
+ }
@@ -0,0 +1,20 @@
1
+ import type { spawn } from "node:child_process";
2
+ import type { accessSync, lstatSync, statSync } from "node:fs";
3
+ import type { DesktopCliRecordRead, DesktopCliRecordIssue } from "./desktop-cli-record.mjs";
4
+ export type DesktopHandoffIssue = DesktopCliRecordIssue | "target-invalid" | "target-self" | "target-unusable" | "spawn-failed";
5
+ export type DesktopCliHandoffPlan =
6
+ | { kind: "continue"; reason: "excluded" | "missing" | "disabled" | "target-missing" | "windows-path-only" }
7
+ | { kind: "error"; issue: DesktopHandoffIssue }
8
+ | { kind: "handoff"; target: string };
9
+ export type DesktopCliHandoffExit =
10
+ | { kind: "continue" }
11
+ | { kind: "error"; issue: "spawn-failed" }
12
+ | { kind: "exit"; code: number; signal: NodeJS.Signals | null };
13
+ export function desktopHandoffExcluded(argv: string[], env?: NodeJS.ProcessEnv): boolean;
14
+ export function planDesktopCliHandoff(input?: {
15
+ argv?: string[]; env?: NodeJS.ProcessEnv; platform?: NodeJS.Platform;
16
+ recordRead?: DesktopCliRecordRead; selfPaths?: (string | undefined)[];
17
+ }, deps?: { stat?: typeof statSync; access?: typeof accessSync; realpath?: (path: string) => string }): DesktopCliHandoffPlan;
18
+ export function runDesktopCliHandoff(plan: Extract<DesktopCliHandoffPlan, { kind: "handoff" }>, launch: {
19
+ argv: string[]; env: NodeJS.ProcessEnv; proof: string; context: string;
20
+ }, deps?: { spawn?: typeof spawn; lstat?: typeof lstatSync; parent?: Pick<NodeJS.Process, "on" | "removeListener">; platform?: NodeJS.Platform }): Promise<DesktopCliHandoffExit>;
@@ -0,0 +1,92 @@
1
+ import { spawn } from "node:child_process";
2
+ import { accessSync, constants, lstatSync, realpathSync, statSync } from "node:fs";
3
+ import { posix, win32 } from "node:path";
4
+ import { readDesktopCliRecord } from "./desktop-cli-record.mjs";
5
+ import { isCodexCliUpdateInspectionArgv } from "../update/codex-cli-update-launch-policy.mjs";
6
+
7
+ const PROOF_PREFIX = "--ocx-internal-launch-proof=";
8
+
9
+ export function desktopHandoffExcluded(argv, env = process.env) {
10
+ const args = argv.filter(arg => !arg.startsWith(PROOF_PREFIX));
11
+ return env.OCX_NO_DESKTOP_HANDOFF === "1"
12
+ || ["update", "uninstall", "remove"].includes(args[0])
13
+ || (args[0] ?? "").startsWith("__")
14
+ || isCodexCliUpdateInspectionArgv(["node", "ocx.mjs", ...args]);
15
+ }
16
+
17
+ export function planDesktopCliHandoff(input = {}, deps = {}) {
18
+ const platform = input.platform ?? process.platform;
19
+ if (platform === "win32") return { kind: "continue", reason: "windows-path-only" };
20
+ const argv = input.argv ?? process.argv.slice(2);
21
+ const env = input.env ?? process.env;
22
+ if (desktopHandoffExcluded(argv, env)) return { kind: "continue", reason: "excluded" };
23
+ const read = input.recordRead ?? readDesktopCliRecord({ env, platform: input.platform });
24
+ if (read.state === "missing" || read.state === "disabled") {
25
+ return { kind: "continue", reason: read.state };
26
+ }
27
+ if (read.state !== "ready") return { kind: "error", issue: read.issue };
28
+ const api = platform === "win32" ? win32 : posix;
29
+ const target = read.record.cliExecutable;
30
+ if (!api.isAbsolute(target)) return { kind: "error", issue: "target-invalid" };
31
+ const stat = deps.stat ?? statSync;
32
+ const access = deps.access ?? accessSync;
33
+ const realpath = deps.realpath ?? realpathSync;
34
+ try {
35
+ if (!stat(target).isFile()) return { kind: "error", issue: "target-invalid" };
36
+ access(target, platform === "win32" ? constants.F_OK : constants.X_OK);
37
+ const physical = realpath(target);
38
+ const key = path => platform === "win32" ? path.toLowerCase() : path;
39
+ for (const self of input.selfPaths ?? [process.argv[1], process.execPath]) {
40
+ if (!self) continue;
41
+ let own;
42
+ try { own = realpath(self); }
43
+ catch (error) { if (error?.code === "ENOENT") continue; throw error; }
44
+ if (key(own) === key(physical)) return { kind: "error", issue: "target-self" };
45
+ }
46
+ return { kind: "handoff", target: physical };
47
+ } catch (error) {
48
+ return error?.code === "ENOENT"
49
+ ? { kind: "continue", reason: "target-missing" }
50
+ : { kind: "error", issue: "target-unusable" };
51
+ }
52
+ }
53
+
54
+ export function runDesktopCliHandoff(plan, launch, deps = {}) {
55
+ const spawnChild = deps.spawn ?? spawn;
56
+ const parent = deps.parent ?? process;
57
+ const platform = deps.platform ?? process.platform;
58
+ const lstat = deps.lstat ?? lstatSync;
59
+ const spawnFailure = error => {
60
+ if (error?.code === "ENOENT") {
61
+ try { lstat(plan.target); }
62
+ catch (checkError) { if (checkError?.code === "ENOENT") return { kind: "continue" }; }
63
+ }
64
+ return { kind: "error", issue: "spawn-failed" };
65
+ };
66
+ const argv = launch.argv.filter(arg => !arg.startsWith(PROOF_PREFIX));
67
+ const env = { ...launch.env, OCX_NO_DESKTOP_HANDOFF: "1", OCX_NODE_LAUNCH_CONTEXT: launch.context };
68
+ delete env.OCX_BUN_RUNTIME_SOURCE;
69
+ delete env.OCX_BUN_RUNTIME_PATH;
70
+ return new Promise(resolveExit => {
71
+ let child;
72
+ try {
73
+ child = spawnChild(plan.target, [`${PROOF_PREFIX}${launch.proof}`, ...argv], {
74
+ stdio: "inherit", shell: false, windowsHide: true, env,
75
+ });
76
+ } catch (error) {
77
+ resolveExit(spawnFailure(error));
78
+ return;
79
+ }
80
+ const forwarded = platform === "win32" ? ["SIGINT", "SIGTERM"] : ["SIGINT", "SIGTERM", "SIGHUP"];
81
+ const handlers = forwarded.map(signal => {
82
+ const handler = () => { try { child.kill(signal); } catch { /* child already exited */ } };
83
+ parent.on(signal, handler);
84
+ return [signal, handler];
85
+ });
86
+ const clear = () => { for (const [signal, handler] of handlers) parent.removeListener(signal, handler); };
87
+ let done = false;
88
+ const finish = result => { if (done) return; done = true; clear(); resolveExit(result); };
89
+ child.once("error", error => finish(spawnFailure(error)));
90
+ child.once("exit", (code, signal) => finish({ kind: "exit", code: code ?? 1, signal }));
91
+ });
92
+ }
@@ -0,0 +1,29 @@
1
+ import type { Stats } from "node:fs";
2
+
3
+ export const DESKTOP_CLI_RECORD_MAX_BYTES: number;
4
+ export type DesktopCliRecord = Readonly<{
5
+ platform: "darwin" | "win32" | "linux";
6
+ kind: "macos-app" | "windows-install" | "linux-deb";
7
+ cliExecutable: string;
8
+ }>;
9
+ export type DesktopCliRecordIssue = "record-invalid" | "record-too-large" | "record-pending" | "record-unreadable" | "record-unsafe";
10
+ export type DesktopCliRecordRead =
11
+ | { state: "missing"; path: string }
12
+ | { state: "disabled"; path: string; cleanupPending: boolean }
13
+ | { state: "ready"; path: string; record: DesktopCliRecord }
14
+ | { state: "invalid" | "unreadable"; path: string; issue: DesktopCliRecordIssue };
15
+ export type DesktopCliRecordOptions = {
16
+ platform?: NodeJS.Platform;
17
+ env?: NodeJS.ProcessEnv;
18
+ home?: string;
19
+ recordPath?: string;
20
+ };
21
+ export function desktopCliRecordPath(options?: DesktopCliRecordOptions): string;
22
+ export type DesktopCliRecordDeps = {
23
+ lstat?: (path: string) => Stats;
24
+ fstat?: (fd: number) => Stats;
25
+ open?: (path: string, flags: number) => number;
26
+ euid?: number;
27
+ checkAcl?: (directory: string, path: string) => boolean;
28
+ };
29
+ export function readDesktopCliRecord(options?: DesktopCliRecordOptions, deps?: DesktopCliRecordDeps): DesktopCliRecordRead;
@@ -0,0 +1,102 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, posix, resolve, win32 } from "node:path";
5
+
6
+ export const DESKTOP_CLI_RECORD_MAX_BYTES = 64 * 1024;
7
+
8
+ export function desktopCliRecordPath(options = {}) {
9
+ const platform = options.platform ?? process.platform;
10
+ const env = options.env ?? process.env;
11
+ const home = options.home ?? (platform === "win32" ? env.USERPROFILE || homedir() : homedir());
12
+ return (platform === "win32" ? win32 : posix).join(home, ".opencodex-desktop", "cli.json");
13
+ }
14
+
15
+ // Keep these lexical rules aligned with Rust's absolute_for in cli_command_record.rs.
16
+ function validTargetPath(path, platform) {
17
+ if ([...path].length > 4096 || /[\x00\r\n]/.test(path)) return false;
18
+ const absolute = platform === "win32"
19
+ ? /^[A-Za-z]:[/\\]/.test(path) || path.startsWith("\\\\") || path.startsWith("//")
20
+ : path.startsWith("/");
21
+ return absolute && !path.split(platform === "win32" ? /[/\\]/ : /\//)
22
+ .some(part => part === "." || part === "..");
23
+ }
24
+
25
+ function noMacAcl(directory, path) {
26
+ const result = spawnSync("/bin/ls", ["-lde", directory, path], {
27
+ encoding: "utf8", timeout: 1000, shell: false, windowsHide: true,
28
+ env: { ...process.env, LC_ALL: "C" },
29
+ });
30
+ if (result.error || result.status !== 0 || result.stderr) return false;
31
+ // Each path has one metadata line; ACL entries add numbered lines and a "+" marker.
32
+ const lines = result.stdout.trimEnd().split("\n");
33
+ return lines.length === 2 && lines.every(line => /^[d-][rwxStTs-]{9}[.@]?\s/.test(line));
34
+ }
35
+
36
+ export function readDesktopCliRecord(options = {}, deps = {}) {
37
+ const platform = options.platform ?? process.platform;
38
+ const path = options.recordPath ?? desktopCliRecordPath(options);
39
+ const directory = dirname(resolve(path));
40
+ const lstat = deps.lstat ?? lstatSync;
41
+ const fstat = deps.fstat ?? fstatSync;
42
+ const open = deps.open ?? openSync;
43
+ const unsafe = () => ({ state: "invalid", path, issue: "record-unsafe" });
44
+ const posixHost = platform !== "win32";
45
+ let fd;
46
+ let checkingSafety = true;
47
+ let absenceAllowed = true;
48
+ try {
49
+ const dirInfo = lstat(directory);
50
+ if (dirInfo.isSymbolicLink() || !dirInfo.isDirectory()) return unsafe();
51
+ const uid = posixHost ? deps.euid ?? process.geteuid?.() : undefined;
52
+ if (posixHost && (uid === undefined || dirInfo.uid !== uid || (dirInfo.mode & 0o077) !== 0)) return unsafe();
53
+ const fileInfo = lstat(path);
54
+ absenceAllowed = false;
55
+ if (fileInfo.isSymbolicLink() || !fileInfo.isFile()
56
+ || (posixHost && (fileInfo.uid !== uid || (fileInfo.mode & 0o077) !== 0))) return unsafe();
57
+ const flags = constants.O_RDONLY | (posixHost ? constants.O_NOFOLLOW | constants.O_NONBLOCK : 0);
58
+ fd = open(path, flags);
59
+ const info = fstat(fd);
60
+ if (!info.isFile() || ["dev", "ino", "uid", "mode"].some(key => info[key] !== fileInfo[key])) return unsafe();
61
+ if (platform === "darwin" && !(deps.checkAcl ?? noMacAcl)(directory, resolve(path))) return unsafe();
62
+ checkingSafety = false;
63
+ if (info.size > DESKTOP_CLI_RECORD_MAX_BYTES) {
64
+ return { state: "invalid", path, issue: "record-too-large" };
65
+ }
66
+ const bytes = Buffer.alloc(DESKTOP_CLI_RECORD_MAX_BYTES + 1);
67
+ let used = 0;
68
+ while (used < bytes.length) {
69
+ const n = readSync(fd, bytes, used, bytes.length - used, null);
70
+ if (n === 0) break;
71
+ used += n;
72
+ }
73
+ if (used > DESKTOP_CLI_RECORD_MAX_BYTES) {
74
+ return { state: "invalid", path, issue: "record-too-large" };
75
+ }
76
+ let value;
77
+ try { value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, used))); }
78
+ catch { return { state: "invalid", path, issue: "record-invalid" }; }
79
+ if (!value || typeof value !== "object" || Array.isArray(value)
80
+ || value.version !== 1 || typeof value.enabled !== "boolean") {
81
+ return { state: "invalid", path, issue: "record-invalid" };
82
+ }
83
+ if (!value.enabled) return { state: "disabled", path, cleanupPending: value.pending != null };
84
+ if (value.pending != null) return { state: "invalid", path, issue: "record-pending" };
85
+ const bundle = value.bundle;
86
+ const kind = { darwin: "macos-app", win32: "windows-install", linux: "linux-deb" }[platform];
87
+ if (!bundle || typeof bundle !== "object" || Array.isArray(bundle)
88
+ || !kind || bundle.platform !== platform || bundle.kind !== kind
89
+ || typeof bundle.cliExecutable !== "string" || !validTargetPath(bundle.cliExecutable, platform)) {
90
+ return { state: "invalid", path, issue: "record-invalid" };
91
+ }
92
+ return { state: "ready", path, record: {
93
+ platform, kind, cliExecutable: bundle.cliExecutable,
94
+ } };
95
+ } catch (error) {
96
+ return error?.code === "ENOENT" && absenceAllowed
97
+ ? { state: "missing", path }
98
+ : checkingSafety ? unsafe() : { state: "unreadable", path, issue: "record-unreadable" };
99
+ } finally {
100
+ if (fd !== undefined) closeSync(fd);
101
+ }
102
+ }
package/src/lib/errors.ts CHANGED
@@ -17,6 +17,7 @@ export const ENCRYPTED_FUNCTION_OUTPUT_REJECTION =
17
17
  * string for a client to be able to tell this apart from a provider rate limit.
18
18
  */
19
19
  export const SEND_BUDGET_EXHAUSTED_CODE = "request_send_budget_exhausted";
20
+ export const SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE = "spend_ledger_storage_unavailable";
20
21
 
21
22
  /** First nonblank string across the canonical upstream error paths, in priority order. */
22
23
  export function upstreamErrorMessageFromPayload(payload: unknown): string | undefined {
@@ -357,6 +358,9 @@ export function isUpstreamResetReplayRefusedMessage(text: string): boolean {
357
358
 
358
359
  export function classifyError(status: number, type: string, message: string): OcxErrorPayload {
359
360
  const text = message.toLowerCase();
361
+ if (type === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE) {
362
+ return { message, type: "server_error", code: SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE };
363
+ }
360
364
  if (type === "previous_response_not_found") {
361
365
  return { message, type: "invalid_request_error", code: "previous_response_not_found" };
362
366
  }
@@ -639,6 +643,24 @@ export function adapterFailureFromMessage(message: string): { httpStatus: number
639
643
  return { httpStatus, error };
640
644
  }
641
645
 
646
+ /** Narrow structured classes carried through a synthesized bare-error terminal. */
647
+ export function recognizedUpstreamError(error: { type?: unknown; code?: unknown }):
648
+ { type: string; code: string; httpStatus: 429 | 503 } | undefined {
649
+ const code = typeof error.code === "string" ? error.code : undefined;
650
+ // An explicit unknown code must not gain a recognized class from its type or copy.
651
+ if (code !== undefined && !["rate_limit_exceeded", "rate_limit_error", "server_is_overloaded", "overloaded_error"].includes(code)) return undefined;
652
+ if (code === "rate_limit_exceeded" || code === "rate_limit_error"
653
+ || (code === undefined && error.type === "rate_limit_error")) {
654
+ return { type: "rate_limit_error", code: code ?? "rate_limit_exceeded", httpStatus: 429 };
655
+ }
656
+ if (code === "server_is_overloaded" || code === "overloaded_error"
657
+ || (code === undefined && error.type === "overloaded_error")) {
658
+ return { type: error.type === "overloaded_error" ? "overloaded_error" : "server_error",
659
+ code: code ?? "server_is_overloaded", httpStatus: 503 };
660
+ }
661
+ return undefined;
662
+ }
663
+
642
664
  /** Map a terminal Responses error object to the HTTP status we record in /api/logs. */
643
665
  export function httpStatusFromTerminalError(error: {
644
666
  type?: string;
@@ -646,10 +668,13 @@ export function httpStatusFromTerminalError(error: {
646
668
  message?: string;
647
669
  } | undefined): number {
648
670
  if (!error) return 502;
671
+ if (error.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE) return 503;
649
672
  if (error.code === "client_closed_request" || error.code === "client_cancelled") return 499;
650
673
  if (isCyberPolicyCode(error.code) || (error.message ? isCyberPolicyMessage(error.message) : false)) {
651
674
  return 400;
652
675
  }
676
+ const recognized = recognizedUpstreamError(error);
677
+ if (recognized) return recognized.httpStatus;
653
678
  if (error.type === "rate_limit_error" || error.code === "rate_limit_exceeded") return 429;
654
679
  if (error.type === "authentication_error" || error.code === "invalid_api_key") return 401;
655
680
  if (
package/src/lib/redact.ts CHANGED
@@ -7,7 +7,7 @@ export const REDACTED_SECRET = "[REDACTED]";
7
7
  * credentials over an unsafe channel (e.g. plaintext non-loopback HTTP) rather than
8
8
  * re-deriving a narrower local list.
9
9
  */
10
- export const SENSITIVE_KEY_PATTERN = /^(?:authorization|proxy-authorization|cookie|set-cookie|set-cookie2|api[-_]?key|x-api-key|x-goog-api-key|x-amz-security-token|access[-_]?token|refresh[-_]?token|id[-_]?token|token|secret|client[-_]?secret|password|profile[-_]?arn|exa[-_]?api[-_]?key)$/i;
10
+ export const SENSITIVE_KEY_PATTERN = /^(?:authorization|proxy-authorization|cookie|set-cookie|set-cookie2|api[-_]?key|x-api-key|x-goog-api-key|x-amz-security-token|copilot[-_]?session[-_]?token|session[-_]?token|access[-_]?token|refresh[-_]?token|id[-_]?token|token|secret|client[-_]?secret|password|profile[-_]?arn|exa[-_]?api[-_]?key)$/i;
11
11
 
12
12
  /**
13
13
  * Colon-labelled credential headers echoed back inside an error body
@@ -40,7 +40,7 @@ export const SENSITIVE_KEY_PATTERN = /^(?:authorization|proxy-authorization|cook
40
40
  // Every letter position also accepts \u0001, the placeholder the fold emits for
41
41
  // an unresolved HTML named reference: `author&ii;zation` is the label with one
42
42
  // character we cannot name, and that is still the label.
43
- const CREDENTIAL_HEADER_LABEL_RAW = "x-api-key|x-goog-api-key|x-amz-security-token|api[_-]?key|apiKey|exa[_-]?api[_-]?key|exaApiKey|access[_-]?token|accessToken|refresh[_-]?token|refreshToken|id[_-]?token|client[_-]?secret|clientSecret|authorization|proxy-authorization|cookie|set-cookie|password|secret|token";
43
+ const CREDENTIAL_HEADER_LABEL_RAW = "copilot[_-]?session[_-]?token|session[_-]?token|sessionToken|x-api-key|x-goog-api-key|x-amz-security-token|api[_-]?key|apiKey|exa[_-]?api[_-]?key|exaApiKey|access[_-]?token|accessToken|refresh[_-]?token|refreshToken|id[_-]?token|client[_-]?secret|clientSecret|authorization|proxy-authorization|cookie|set-cookie|password|secret|token";
44
44
 
45
45
  const CREDENTIAL_HEADER_LABEL = CREDENTIAL_HEADER_LABEL_RAW
46
46
  .replace(/(?<![\[\\])([A-Za-z])(?![\]\-])/g, "[$1\u0001]");
@@ -194,6 +194,8 @@ export interface RequestExecutionBudget extends TransientSendBudget {
194
194
  readonly alternateTargetSends: number;
195
195
  readonly targetTransitions: number;
196
196
  readonly lastTargetKey: string | undefined;
197
+ /** Seed a fresh derived scope with the actual endpoint of its already-paid initial send. */
198
+ bindPrepaidTarget?(targetKey: string): void;
197
199
  /**
198
200
  * Spend one operator-granted replacement for an AMBIGUOUS failure of this logical request,
199
201
  * up to `limit`. False once the request has none left.
@@ -390,6 +392,7 @@ function createAmbiguousResendGrant(): Pick<SharedSendLedger, "claimAmbiguousRes
390
392
  };
391
393
  }
392
394
 
395
+ /** Keep target-local transition state over the shared ledger and its single-use bookings. */
393
396
  function createRequestExecutionBudgetWithLedger(
394
397
  policy: RequestExecutionBudgetPolicy,
395
398
  logicalRequestId: string | undefined,
@@ -441,6 +444,11 @@ function createRequestExecutionBudgetWithLedger(
441
444
  get alternateTargetSends() { return alternateTargetSends; },
442
445
  get targetTransitions() { return targetTransitions; },
443
446
  get lastTargetKey() { return lastTargetKey; },
447
+ /** Seed only a fresh scope; never reset recovery history on an existing target. */
448
+ bindPrepaidTarget(targetKey: string): void {
449
+ // Only a fresh scope may bind: this is initial identity, never a recovery rebase.
450
+ if (lastTargetKey === undefined) lastTargetKey = targetKey;
451
+ },
444
452
  remainingBaseSends(cap: number): number {
445
453
  const capped = Number.isFinite(cap) ? Math.trunc(cap) : 0;
446
454
  return Math.max(0, Math.min(capped, policy.baseSendAllowance - counter.spent));