@bitkyc08/opencodex 2.83.0 → 2.86.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 (405) hide show
  1. package/README.md +0 -4
  2. package/bin/ocx.mjs +61 -3
  3. package/gui/dist/assets/App-BjlttVLy.js +58 -0
  4. package/gui/dist/assets/Tray-DyR9yBee.js +1 -0
  5. package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
  6. package/gui/dist/assets/index-BMssP4k3.js +94 -0
  7. package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +2 -3
  10. package/src/adapters/anthropic/beta-allowlist.ts +87 -8
  11. package/src/adapters/anthropic/client-identity.ts +1 -1
  12. package/src/adapters/anthropic/passthrough.ts +33 -4
  13. package/src/adapters/azure.ts +19 -1
  14. package/src/adapters/client-fingerprint.ts +24 -0
  15. package/src/adapters/coding-agent/protocol.ts +15 -8
  16. package/src/adapters/coding-agent/turn.ts +22 -9
  17. package/src/adapters/cursor/arg-codec.ts +17 -5
  18. package/src/adapters/cursor/catalog.ts +8 -0
  19. package/src/adapters/cursor/effort-map.ts +3 -0
  20. package/src/adapters/cursor/freeform-args.ts +75 -0
  21. package/src/adapters/cursor/protobuf-events.ts +36 -23
  22. package/src/adapters/google-antigravity-wire.ts +22 -1
  23. package/src/adapters/google.ts +2 -2
  24. package/src/adapters/ollama-native.ts +29 -1
  25. package/src/adapters/openai-chat/messages.ts +8 -4
  26. package/src/adapters/openai-chat/response-events.ts +59 -1
  27. package/src/adapters/openai-chat.ts +8 -5
  28. package/src/adapters/openai-responses/empty-assistant.ts +32 -0
  29. package/src/adapters/openai-responses/passthrough.ts +3 -0
  30. package/src/adapters/openai-responses/request-strips.ts +10 -2
  31. package/src/adapters/qoder/adapter.ts +56 -15
  32. package/src/advisor/consult.ts +220 -0
  33. package/src/advisor/context.ts +226 -0
  34. package/src/advisor/disclosure.ts +20 -0
  35. package/src/advisor/runtime.ts +305 -0
  36. package/src/advisor/settings.ts +135 -0
  37. package/src/advisor/state.ts +369 -0
  38. package/src/advisor/synthetic-tool.ts +33 -0
  39. package/src/bridge/errors.ts +24 -0
  40. package/src/bridge.ts +1 -1
  41. package/src/chat/inbound.ts +30 -6
  42. package/src/chat/outbound.ts +5 -0
  43. package/src/claude/context-windows.ts +54 -1
  44. package/src/claude/desktop-3p-library.ts +43 -3
  45. package/src/claude/desktop-3p.ts +22 -8
  46. package/src/claude/desktop-picker-profile.ts +11 -4
  47. package/src/claude/desktop-profile.ts +10 -1
  48. package/src/claude/desktop-remote-store-io.ts +5 -3
  49. package/src/claude/inbound.ts +121 -50
  50. package/src/claude/intercept/connect-proxy.ts +10 -3
  51. package/src/claude/intercept/local-ca.ts +8 -1
  52. package/src/claude/intercept/picker-ca-persistence.ts +3 -1
  53. package/src/claude/intercept/picker-listener.ts +49 -1
  54. package/src/claude/intercept/settings-no-proxy.ts +146 -0
  55. package/src/claude/intercept/settings.ts +101 -9
  56. package/src/claude/model-default-settings.ts +154 -0
  57. package/src/claude/model-info.ts +7 -7
  58. package/src/claude/outbound.ts +13 -6
  59. package/src/cli/account-api.ts +65 -0
  60. package/src/cli/account-auth.ts +26 -2
  61. package/src/cli/account.ts +41 -5
  62. package/src/cli/advisor.ts +151 -0
  63. package/src/cli/auxiliary-listener-preflight.ts +37 -0
  64. package/src/cli/capabilities-access-remote.ts +8 -0
  65. package/src/cli/capabilities-agents-routing.ts +18 -0
  66. package/src/cli/capabilities-base.ts +14 -2
  67. package/src/cli/catalog-command-result.ts +2 -1
  68. package/src/cli/chatgpt-command.ts +35 -14
  69. package/src/cli/claude.ts +60 -11
  70. package/src/cli/config-command.ts +8 -2
  71. package/src/cli/debug.ts +8 -0
  72. package/src/cli/dispatch.ts +21 -4
  73. package/src/cli/doctor.ts +9 -0
  74. package/src/cli/ensure-desktop-cold-start.ts +329 -0
  75. package/src/cli/export-command.ts +15 -2
  76. package/src/cli/help.ts +2 -1
  77. package/src/cli/hub-disable.ts +99 -0
  78. package/src/cli/hub.ts +4 -2
  79. package/src/cli/index.ts +26 -8
  80. package/src/cli/integration-plan-dto.ts +2 -0
  81. package/src/cli/link-child.ts +104 -0
  82. package/src/cli/link-enrollment.ts +96 -0
  83. package/src/cli/link.ts +39 -24
  84. package/src/cli/observe-stream.ts +6 -1
  85. package/src/cli/observe.ts +10 -2
  86. package/src/cli/provider.ts +12 -0
  87. package/src/cli/registry.ts +20 -7
  88. package/src/cli/restart-scope.ts +3 -1
  89. package/src/cli/runtime-api.ts +27 -2
  90. package/src/cli/status.ts +104 -20
  91. package/src/client/connect.ts +39 -4
  92. package/src/client/hub-client.ts +14 -7
  93. package/src/client/hub-state.ts +25 -14
  94. package/src/client/link-home.ts +248 -0
  95. package/src/client/link-management-relay.ts +61 -0
  96. package/src/client/link-relay.ts +6 -1
  97. package/src/client/link-status.ts +14 -0
  98. package/src/client/machine-api.ts +14 -7
  99. package/src/client/machine-listener.ts +29 -4
  100. package/src/clients/config-export/contracts.ts +1 -0
  101. package/src/clients/config-export/model-metadata.ts +3 -2
  102. package/src/clients/config-export/qoder.ts +93 -0
  103. package/src/clients/config-export.ts +59 -16
  104. package/src/clients/omo-role-models.ts +6 -3
  105. package/src/codex/agent-role-models.ts +26 -8
  106. package/src/codex/auth-api/account-list.ts +17 -2
  107. package/src/codex/auth-api/account-selection.ts +17 -7
  108. package/src/codex/auth-api/pool-mode-gate.ts +5 -2
  109. package/src/codex/auth-api/routes.ts +5 -1
  110. package/src/codex/auth-context.ts +79 -5
  111. package/src/codex/autostart-health.ts +7 -1
  112. package/src/codex/catalog/build-entries.ts +11 -2
  113. package/src/codex/catalog/bundled.ts +5 -1
  114. package/src/codex/catalog/configured-tiers.ts +50 -0
  115. package/src/codex/catalog/effort.ts +6 -3
  116. package/src/codex/catalog/metadata.ts +4 -2
  117. package/src/codex/catalog/model-hints.ts +21 -3
  118. package/src/codex/catalog/native-models.ts +16 -2
  119. package/src/codex/catalog/native-ultrafast.ts +106 -0
  120. package/src/codex/catalog/parsing.ts +5 -0
  121. package/src/codex/catalog/provider-models.ts +4 -0
  122. package/src/codex/catalog/retained-sync.ts +2 -0
  123. package/src/codex/catalog/routed-gather.ts +22 -4
  124. package/src/codex/catalog/vendor-context.ts +82 -0
  125. package/src/codex/config-write-lock.ts +40 -2
  126. package/src/codex/convergence.ts +2 -0
  127. package/src/codex/desktop-app/types.ts +2 -0
  128. package/src/codex/desktop-app/windows.ts +29 -16
  129. package/src/codex/desktop-app-restart.ts +4 -2
  130. package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
  131. package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
  132. package/src/codex/desktop-compatibility/connection-store.ts +87 -0
  133. package/src/codex/desktop-compatibility/installed-build.ts +55 -0
  134. package/src/codex/desktop-compatibility/json-body.ts +29 -0
  135. package/src/codex/desktop-compatibility/native-identity.ts +62 -0
  136. package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
  137. package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
  138. package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
  139. package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
  140. package/src/codex/desktop-compatibility/runtime.ts +173 -0
  141. package/src/codex/desktop-compatibility/service.ts +30 -0
  142. package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
  143. package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
  144. package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
  145. package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
  146. package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
  147. package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
  148. package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
  149. package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
  150. package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
  151. package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
  152. package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
  153. package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
  154. package/src/codex/history-provider.ts +1 -1
  155. package/src/codex/inject/config-write-section.ts +4 -1
  156. package/src/codex/inject/remove.ts +18 -6
  157. package/src/codex/journal.ts +16 -1
  158. package/src/codex/main-account.ts +99 -15
  159. package/src/codex/native-main-refresh-mode.ts +217 -0
  160. package/src/codex/native-profile-startup.ts +4 -1
  161. package/src/codex/prompt-journal.ts +20 -2
  162. package/src/codex/prompt-layers/encoding.ts +49 -0
  163. package/src/codex/prompt-layers/import-source.ts +30 -0
  164. package/src/codex/prompt-layers/toml-edit.ts +269 -37
  165. package/src/codex/prompt-layers/toml-read.ts +52 -4
  166. package/src/codex/prompt-layers.ts +383 -20
  167. package/src/codex/prompt-lock-claim.ts +13 -3
  168. package/src/codex/prompt-lock-evidence.ts +3 -3
  169. package/src/codex/prompt-lock-owner.ts +14 -6
  170. package/src/codex/prompt-text-probe.ts +34 -5
  171. package/src/codex/quota.ts +13 -6
  172. package/src/codex/role-auto-assign.ts +2 -5
  173. package/src/codex/routing/active-account.ts +21 -0
  174. package/src/codex/routing/health-store.ts +1 -1
  175. package/src/codex/routing/pool-recovery.ts +79 -0
  176. package/src/codex/routing.ts +1 -1
  177. package/src/codex/runtime.ts +196 -8
  178. package/src/codex/shim-diagnostics.ts +6 -0
  179. package/src/codex/shim-overlay.ts +15 -6
  180. package/src/codex/shim.ts +1 -1
  181. package/src/combos/failover.ts +3 -0
  182. package/src/config/api-key-reconcile.ts +34 -0
  183. package/src/config/diagnostics.ts +18 -1
  184. package/src/config/feature-flags.ts +5 -0
  185. package/src/config/live-reconcile.ts +56 -29
  186. package/src/config/live-topology.ts +29 -0
  187. package/src/config/load-degrade.ts +8 -2
  188. package/src/config/paths.ts +13 -3
  189. package/src/config/salvage.ts +3 -1
  190. package/src/config/schema/config-schema.ts +19 -0
  191. package/src/config/schema/desktop-compatibility.ts +9 -0
  192. package/src/config/schema/leaf-validators.ts +1 -0
  193. package/src/generated/compatibility-version.json +713 -285
  194. package/src/generated/model-metadata.ts +1 -1
  195. package/src/grok/grpc-web.ts +23 -1
  196. package/src/grok/reset-coupon-ledger.ts +75 -5
  197. package/src/grok/reset-coupons.ts +4 -2
  198. package/src/integrations/catalog-refresh.ts +1 -1
  199. package/src/integrations/config-io.ts +20 -2
  200. package/src/integrations/journal.ts +15 -1
  201. package/src/integrations/mutation-plan.ts +14 -7
  202. package/src/integrations/registry.ts +52 -11
  203. package/src/integrations/state.ts +19 -7
  204. package/src/integrations/store.ts +3 -0
  205. package/src/integrations/target.ts +42 -5
  206. package/src/integrations/writer.ts +24 -7
  207. package/src/lib/advisor-activation.ts +16 -0
  208. package/src/lib/app-owned-memory-stores.ts +4 -0
  209. package/src/lib/app-owned-memory.ts +11 -0
  210. package/src/lib/background-work.ts +65 -0
  211. package/src/lib/bounded-file-read.ts +58 -0
  212. package/src/lib/bun-path-runtime.d.mts +10 -1
  213. package/src/lib/bun-path-runtime.mjs +62 -3
  214. package/src/lib/desktop-proxy-route.ts +30 -0
  215. package/src/lib/desktop-upstream-tunnel.ts +107 -0
  216. package/src/lib/errors.ts +7 -0
  217. package/src/lib/json-byte-size.ts +31 -0
  218. package/src/lib/local-internal-call-capability.ts +55 -0
  219. package/src/lib/optional-shutdown-hooks.ts +35 -0
  220. package/src/lib/outbound-credential-registry.ts +210 -0
  221. package/src/lib/package-tree-integrity.ts +2 -2
  222. package/src/lib/passive-read.ts +35 -0
  223. package/src/lib/shadow-call.ts +44 -5
  224. package/src/lib/socks5-fetch.ts +48 -71
  225. package/src/lib/socks5-handshake.ts +112 -0
  226. package/src/lib/translator-budget.ts +7 -1
  227. package/src/link/codex-settings.ts +155 -0
  228. package/src/link/management-routes.ts +89 -0
  229. package/src/link/remote-probe.ts +38 -0
  230. package/src/link/ssh-argv.ts +26 -5
  231. package/src/link/ssh-runner.ts +58 -27
  232. package/src/link/status-projection.ts +22 -4
  233. package/src/link/supervisor.ts +144 -49
  234. package/src/link/tunnel-state.ts +11 -4
  235. package/src/oauth/anthropic-continuity.ts +4 -3
  236. package/src/oauth/anthropic-identity.ts +13 -2
  237. package/src/oauth/anthropic-model-quota.ts +13 -1
  238. package/src/oauth/anthropic-routing.ts +230 -23
  239. package/src/oauth/anthropic.ts +18 -4
  240. package/src/oauth/generic-account-failover.ts +29 -5
  241. package/src/oauth/google-antigravity.ts +24 -7
  242. package/src/oauth/index.ts +28 -17
  243. package/src/oauth/kiro-account-identity.ts +106 -0
  244. package/src/oauth/kiro-device-login.ts +2 -1
  245. package/src/oauth/kiro.ts +55 -6
  246. package/src/oauth/plan.ts +29 -0
  247. package/src/oauth/store.ts +40 -3
  248. package/src/oauth/types.ts +2 -0
  249. package/src/plugins/loader.ts +11 -0
  250. package/src/protocols/codecs/chat-ir.ts +467 -0
  251. package/src/protocols/codecs/chat.ts +10 -1
  252. package/src/protocols/codecs/messages-ir.ts +500 -0
  253. package/src/protocols/features.ts +10 -2
  254. package/src/protocols/path.ts +11 -1
  255. package/src/protocols/plan-snapshot.ts +39 -16
  256. package/src/protocols/plan.ts +9 -1
  257. package/src/protocols/settings.ts +8 -0
  258. package/src/providers/anthropic-billing-ownership.ts +74 -0
  259. package/src/providers/anthropic-billing-recovery.ts +31 -0
  260. package/src/providers/anthropic-billing-refusal.ts +55 -0
  261. package/src/providers/anthropic-reset-admission.ts +62 -0
  262. package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
  263. package/src/providers/antigravity-account-roster.ts +102 -0
  264. package/src/providers/antigravity-models.ts +85 -7
  265. package/src/providers/api-key-resolve.ts +17 -2
  266. package/src/providers/key-failover.ts +52 -5
  267. package/src/providers/model-rename-fields.ts +2 -0
  268. package/src/providers/model-rename-migration.ts +5 -3
  269. package/src/providers/model-rename-startup.ts +5 -3
  270. package/src/providers/quota/account-cache.ts +11 -0
  271. package/src/providers/quota/anthropic-account-quota.ts +4 -2
  272. package/src/providers/quota/report-cache.ts +14 -0
  273. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  274. package/src/providers/quota.ts +4 -1
  275. package/src/providers/registry/entries-core.ts +21 -20
  276. package/src/providers/registry/entries-extended.ts +19 -1
  277. package/src/providers/registry/model-seeds.ts +17 -0
  278. package/src/providers/request-pacing.ts +9 -0
  279. package/src/providers/stale-no-vision-seed-migration.ts +72 -0
  280. package/src/responses/compaction.ts +14 -0
  281. package/src/responses/parser.ts +37 -20
  282. package/src/responses/state.ts +32 -29
  283. package/src/routing/history/build-file.ts +92 -0
  284. package/src/routing/history/indexer.ts +248 -216
  285. package/src/routing/history/ingest.ts +238 -0
  286. package/src/routing/history/rebuild-job.ts +111 -0
  287. package/src/routing/history/rebuild-protocol.ts +15 -0
  288. package/src/routing/history/rebuild-worker.ts +34 -0
  289. package/src/server/admission-model-scope.ts +4 -1
  290. package/src/server/antigravity-direct-eligibility.ts +208 -0
  291. package/src/server/auth-cors.ts +46 -0
  292. package/src/server/chat-completions.ts +75 -9
  293. package/src/server/claude-messages.ts +110 -20
  294. package/src/server/configured-credentials.ts +194 -0
  295. package/src/server/index/desktop-compatibility-startup.ts +56 -0
  296. package/src/server/index/link-management.ts +58 -0
  297. package/src/server/index/optional-listeners.ts +5 -1
  298. package/src/server/index/serve-options.ts +15 -2
  299. package/src/server/index/startup-warnings.ts +9 -0
  300. package/src/server/index/websocket-handler.ts +9 -0
  301. package/src/server/index.ts +13 -10
  302. package/src/server/inference/client-encoder-delivery.ts +6 -5
  303. package/src/server/lifecycle.ts +2 -1
  304. package/src/server/login-shell-path.ts +146 -0
  305. package/src/server/management/advisor-routes.ts +191 -0
  306. package/src/server/management/agent-settings-routes.ts +50 -14
  307. package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
  308. package/src/server/management/codex-agent-role-routes.ts +19 -6
  309. package/src/server/management/codex-prompt-routes.ts +136 -5
  310. package/src/server/management/codex-role-auto-assign.ts +1 -1
  311. package/src/server/management/companion-routes.ts +4 -0
  312. package/src/server/management/config-routes.ts +2 -2
  313. package/src/server/management/context.ts +4 -0
  314. package/src/server/management/desktop-compatibility-routes.ts +46 -0
  315. package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
  316. package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
  317. package/src/server/management/grok-coupon-routes.ts +78 -27
  318. package/src/server/management/link-enrollment.ts +192 -0
  319. package/src/server/management/link-route-state.ts +185 -0
  320. package/src/server/management/link-routes.ts +82 -402
  321. package/src/server/management/model-routes.ts +2 -2
  322. package/src/server/management/model-rows.ts +3 -1
  323. package/src/server/management/protocol-settings-patch.ts +1 -0
  324. package/src/server/management/provider-overwrite-carry.ts +10 -0
  325. package/src/server/management/provider-routes.ts +1 -0
  326. package/src/server/management/route-registry.ts +16 -3
  327. package/src/server/management/shared.ts +3 -0
  328. package/src/server/management/sibling-guard.ts +1 -0
  329. package/src/server/management-api.ts +9 -1
  330. package/src/server/messages-antigravity-direct.ts +195 -0
  331. package/src/server/messages-native.ts +73 -43
  332. package/src/server/relay-eager.ts +2 -0
  333. package/src/server/request-log-filter.ts +6 -1
  334. package/src/server/request-log-terminal-status.ts +63 -6
  335. package/src/server/request-log.ts +7 -0
  336. package/src/server/responses/adapter-continuation.ts +35 -18
  337. package/src/server/responses/adapter-delivery.ts +24 -2
  338. package/src/server/responses/adapter-dispatch.ts +261 -81
  339. package/src/server/responses/advisor-plan-slot.ts +27 -0
  340. package/src/server/responses/advisor-slot.ts +336 -0
  341. package/src/server/responses/code-mode-collab-surface.ts +313 -0
  342. package/src/server/responses/codex-ws-exchange.ts +147 -6
  343. package/src/server/responses/codex-ws-wire.ts +203 -0
  344. package/src/server/responses/collaboration.ts +36 -7
  345. package/src/server/responses/compact.ts +126 -48
  346. package/src/server/responses/context-overflow.ts +13 -1
  347. package/src/server/responses/core-codex-account.ts +105 -1
  348. package/src/server/responses/core-combo-failure.ts +24 -1
  349. package/src/server/responses/core-combo.ts +28 -2
  350. package/src/server/responses/core-errors.ts +29 -1
  351. package/src/server/responses/core-lifetime.ts +2 -0
  352. package/src/server/responses/core-normalize.ts +11 -0
  353. package/src/server/responses/core-options.ts +48 -0
  354. package/src/server/responses/core-replay.ts +7 -4
  355. package/src/server/responses/core.ts +1 -1
  356. package/src/server/responses/deferred-collab-surface.ts +158 -0
  357. package/src/server/responses/deferred-send-reporter.ts +25 -0
  358. package/src/server/responses/fetch-helpers.ts +12 -1
  359. package/src/server/responses/passthrough-delivery.ts +20 -11
  360. package/src/server/responses/passthrough-dispatch.ts +29 -7
  361. package/src/server/responses/request-prepare.ts +74 -9
  362. package/src/server/responses/request-transport.ts +22 -8
  363. package/src/server/responses/run-turn-execution.ts +70 -8
  364. package/src/server/responses/sidecar-execution.ts +89 -5
  365. package/src/server/responses/terminal-guard.ts +2 -1
  366. package/src/server/responses/ws-upstream.ts +9 -1
  367. package/src/server/responses-undeclared-tool-guard.ts +30 -9
  368. package/src/server/startup-action-control.ts +38 -4
  369. package/src/server/startup-health-cache.ts +5 -0
  370. package/src/server/system-env-shell.ts +7 -2
  371. package/src/server/system-env.ts +11 -1
  372. package/src/service/cli.ts +5 -1
  373. package/src/service/desktop-startup.ts +161 -34
  374. package/src/service/desktop-supervision.d.mts +27 -1
  375. package/src/service/desktop-supervision.mjs +111 -5
  376. package/src/service/health-sentinel-launch.ts +176 -0
  377. package/src/service/health-sentinel-main.ts +466 -0
  378. package/src/service/health-sentinel-policy.ts +241 -0
  379. package/src/service/health-sentinel-probe.ts +71 -0
  380. package/src/service/health-sentinel-windows.ts +87 -0
  381. package/src/service/linux-linger.ts +20 -0
  382. package/src/service/logging.ts +72 -0
  383. package/src/tray/windows-tray.ps1 +43 -24
  384. package/src/types/config.ts +68 -6
  385. package/src/types/provider.ts +7 -0
  386. package/src/types/request.ts +15 -0
  387. package/src/types/tools.ts +2 -0
  388. package/src/update/async-check.ts +1 -1
  389. package/src/update/badge.ts +2 -2
  390. package/src/update/index.ts +88 -15
  391. package/src/update/job-supervised-restart.ts +107 -0
  392. package/src/update/job.ts +32 -32
  393. package/src/update/notify.ts +1 -1
  394. package/src/update/refresh-scheduler.ts +2 -2
  395. package/src/update/supervised-recovery.d.mts +27 -0
  396. package/src/update/supervised-recovery.mjs +157 -0
  397. package/src/usage/cache-diagnostic.ts +2 -0
  398. package/src/usage/expected-prices.ts +16 -0
  399. package/src/usage/log.ts +19 -1
  400. package/src/web-search/gemini-executor.ts +2 -2
  401. package/assets/sponsors/packycode.png +0 -0
  402. package/gui/dist/assets/App-Ba8uG696.js +0 -54
  403. package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
  404. package/gui/dist/assets/index-BHRZdJuv.js +0 -94
  405. package/gui/dist/assets/quota-summary-DiJO6sYY.js +0 -1
package/src/lib/errors.ts CHANGED
@@ -18,6 +18,13 @@ export const ENCRYPTED_FUNCTION_OUTPUT_REJECTION =
18
18
  */
19
19
  export const SEND_BUDGET_EXHAUSTED_CODE = "request_send_budget_exhausted";
20
20
  export const SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE = "spend_ledger_storage_unavailable";
21
+ /**
22
+ * Wire code for the undeclared-tool guard's deterministic refusal. Endpoint translators
23
+ * read it to keep the refusal non-retryable instead of folding it into the transient
24
+ * upstream-error class. Declared here rather than in the guard module because the shared
25
+ * envelope formatter decides whether the identity survives serialization.
26
+ */
27
+ export const UNDECLARED_TOOL_CALL_ERROR_CODE = "undeclared_tool_call";
21
28
 
22
29
  /** First nonblank string across the canonical upstream error paths, in priority order. */
23
30
  export function upstreamErrorMessageFromPayload(payload: unknown): string | undefined {
@@ -1,5 +1,36 @@
1
1
  import { TRANSLATOR_MAX_TURN_BYTES, TranslatorBudgetExceededError } from "./translator-budget";
2
2
 
3
+ /** Count one JSON string formed from parts without joining, serializing or encoding it. */
4
+ export function jsonStringPartsUtf8Bytes(parts: readonly string[], limit = TRANSLATOR_MAX_TURN_BYTES): number {
5
+ let bytes = 0;
6
+ let highSurrogate = false;
7
+ const add = (count: number) => {
8
+ if (count > limit - bytes) throw new TranslatorBudgetExceededError("request_copies", limit);
9
+ bytes += count;
10
+ };
11
+ add(2); // The joined string has one pair of JSON quotes.
12
+ for (const text of parts) {
13
+ if (text.length > limit - bytes) throw new TranslatorBudgetExceededError("request_copies", limit);
14
+ for (let i = 0; i < text.length; i++) {
15
+ const code = text.charCodeAt(i);
16
+ if (highSurrogate) {
17
+ highSurrogate = false;
18
+ if (code >= 0xdc00 && code <= 0xdfff) { add(4); continue; }
19
+ add(6);
20
+ }
21
+ if (code >= 0xd800 && code <= 0xdbff) highSurrogate = true;
22
+ else if (code === 0x22 || code === 0x5c || code === 8 || code === 9 || code === 10 || code === 12 || code === 13) add(2);
23
+ else if (code < 0x20) add(6);
24
+ else if (code < 0x80) add(1);
25
+ else if (code < 0x800) add(2);
26
+ else if (code >= 0xdc00 && code <= 0xdfff) add(6);
27
+ else add(3);
28
+ }
29
+ }
30
+ if (highSurrogate) add(6);
31
+ return bytes;
32
+ }
33
+
3
34
  /** Measure plain JSON data without allocating its serialized string or UTF-8 copy. */
4
35
  export function jsonUtf8Bytes(value: unknown, limit = TRANSLATOR_MAX_TURN_BYTES): number {
5
36
  let bytes = 0;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Process-local internal-call capability.
3
+ *
4
+ * Some requests ARE the proxy itself talking to its own data plane: the advisor sidecar's
5
+ * loopback consultation is the first user. Such a request must be recognized as INTERNAL without
6
+ * trusting anything the client controls. A client-supplied header value is not evidence — any
7
+ * external caller can send it — and neither is the peer address: a public listener reached
8
+ * through Docker, WSL, a tunnel, or port forwarding can present as loopback on the last hop, as
9
+ * the local-management attestation notes.
10
+ *
11
+ * The authority is therefore process-owned: a 256-bit random value minted once per process, kept
12
+ * in memory only. It is never written to config or disk, never logged, never returned by the
13
+ * management API, never placed in usage or request metadata, and never forwarded upstream. A new
14
+ * process mints a new value, so a token captured from an older process is worthless. Comparison
15
+ * is timing-safe and shape-checked, reusing the same secret shape as the local attestation
16
+ * module.
17
+ *
18
+ * The same primitive is what the vision-describe fence would need for the same reason; wiring
19
+ * that surface is deliberately left out of this change (recorded as a pre-existing analogous
20
+ * issue) so the change stays scoped.
21
+ */
22
+ import { randomBytes, timingSafeEqual } from "node:crypto";
23
+ import { isLocalAttestationSecret } from "./local-management-attestation";
24
+
25
+ /** Header an internal sidecar presents on its own loopback request. */
26
+ export const ADVISOR_INTERNAL_CAPABILITY_HEADER = "x-opencodex-advisor-internal";
27
+
28
+ let processCapability: string | null = null;
29
+
30
+ /**
31
+ * The current process's internal-call capability. Minted lazily on first use (one random draw,
32
+ * no I/O), so an install that never enables the advisor never generates one.
33
+ */
34
+ export function internalCallCapability(): string {
35
+ if (processCapability === null) processCapability = randomBytes(32).toString("base64url");
36
+ return processCapability;
37
+ }
38
+
39
+ /** Test seam: install a deterministic value, or `null` to force a fresh mint. */
40
+ export function setInternalCallCapabilityForTests(value: string | null): void {
41
+ processCapability = value;
42
+ }
43
+
44
+ /**
45
+ * True only when the supplied header value IS this process's capability. Shape-checked first so
46
+ * a malformed value cannot reach the comparison, then compared in constant time.
47
+ */
48
+ export function isInternalCallCapability(supplied: string | null | undefined): boolean {
49
+ if (typeof supplied !== "string" || !isLocalAttestationSecret(supplied)) return false;
50
+ if (processCapability === null) return false;
51
+ const expected = processCapability;
52
+ const suppliedBytes = Buffer.from(supplied);
53
+ const expectedBytes = Buffer.from(expected);
54
+ return suppliedBytes.length === expectedBytes.length && timingSafeEqual(suppliedBytes, expectedBytes);
55
+ }
@@ -70,5 +70,40 @@ export function runOptionalShutdownHooks(): void {
70
70
  /** Test-only reset so an isolated lifecycle test does not inherit registrations. */
71
71
  export function resetOptionalShutdownHooksForTests(): void {
72
72
  hooks.clear();
73
+ drainStartHooks.clear();
74
+ drainStartHooksRan = false;
73
75
  hooksRan = false;
74
76
  }
77
+
78
+ const drainStartHooks = new Map<string, ShutdownHook>();
79
+ let drainStartHooksRan = false;
80
+
81
+ /**
82
+ * Register work that must run the moment a drain begins, before in-flight turns are awaited.
83
+ * The service health sentinel (#6671) uses it: a deliberate stop whose drain stalls must stay a
84
+ * stop, so the sentinel is withdrawn before the drain can look unresponsive. Same contract as
85
+ * {@link registerOptionalShutdownHook}: keyed, synchronous, best-effort, detachable.
86
+ */
87
+ export function registerDrainStartHook(key: string, hook: ShutdownHook): () => void {
88
+ drainStartHooks.set(key, hook);
89
+ return () => {
90
+ if (drainStartHooks.get(key) === hook) drainStartHooks.delete(key);
91
+ };
92
+ }
93
+
94
+ /** Run every drain-start hook. Never throws. */
95
+ export function runDrainStartHooks(): void {
96
+ drainStartHooksRan = true;
97
+ for (const [key, hook] of [...drainStartHooks]) {
98
+ try {
99
+ hook();
100
+ } catch (err) {
101
+ console.warn(`[shutdown] drain-start hook "${key}" failed:`, err instanceof Error ? err.message : err);
102
+ }
103
+ }
104
+ }
105
+
106
+ /** Whether a drain has begun in this process; work that must not start mid-drain checks this. */
107
+ export function didRunDrainStartHooks(): boolean {
108
+ return drainStartHooksRan;
109
+ }
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Process-local record of credential values this process has handed to an upstream.
3
+ *
4
+ * Upstream diagnostics (error codes, request ids) are provider-controlled. A gateway can echo a
5
+ * credential it received back into those fields, and a credential with no recognizable format slips
6
+ * past pattern redaction. Configured credentials are read directly at capture time (see
7
+ * server/configured-credentials); this registry adds the values that only exist at send time, such
8
+ * as a client-forwarded bearer or a plugin-added header.
9
+ *
10
+ * Two tiers keep ordinary traffic from disabling diagnostics. Values of credential-named headers are
11
+ * strict: they match at any length, and once one is evicted coverage is incomplete for the rest of
12
+ * the process, so callers fail closed. Every other header value is best effort: it matches only at
13
+ * eight characters or more and its eviction never disables diagnostics, so per-turn metadata cannot
14
+ * exhaust anything that matters. Correlation ids (request ids, trace context) are left out of the
15
+ * best-effort tier because a gateway legitimately echoes them as its request id; a configured header
16
+ * of that name is still covered by the configured set. Values are held in memory only: nothing here
17
+ * is serialized, persisted or exposed.
18
+ */
19
+ import { SENSITIVE_KEY_PATTERN } from "./redact";
20
+
21
+ /** Containment only for credentials long enough that a diagnostic cannot contain one by chance. */
22
+ export const CREDENTIAL_SUBSTRING_MIN_LENGTH = 8;
23
+ const CREDENTIAL_HEADER_NAME = /api[-_]?key|secret|passw|credential|auth|(?:^|[-_])token$/i;
24
+ const CORRELATION_HEADER_NAME = /^(?:x-(?:client-)?request-id|request-id|traceparent|tracestate)$/i;
25
+ const AUTH_SCHEMES = new Set(["bearer", "basic", "token", "digest", "apikey", "api-key", "key", "sso-key", "negotiate", "dpop"]);
26
+ const TOKEN_CHAR = /[!#$%&'*+.^_`|~0-9A-Za-z-]/;
27
+ const BASIC_BASE64 = /^[A-Za-z0-9+/]+={0,2}$/;
28
+ const PRINTABLE_USER_PASS = /^[\x20-\x7e]*:[\x20-\x7e]*$/;
29
+
30
+ /** True for a header (or config field) name whose value is a credential by its name. */
31
+ export function isCredentialName(name: string): boolean {
32
+ return SENSITIVE_KEY_PATTERN.test(name) || CREDENTIAL_HEADER_NAME.test(name);
33
+ }
34
+
35
+ /**
36
+ * Values of RFC 9110 auth-params (token BWS "=" BWS ( token / quoted-string )), cookie pairs and
37
+ * other name=value pairs, in one linear pass: every character is visited a bounded number of times,
38
+ * so a long header value cannot stall the event loop.
39
+ */
40
+ function parameterValues(value: string): string[] {
41
+ const out: string[] = [];
42
+ const n = value.length;
43
+ let unclosedFrom = n; // no closing quote exists for any opening quote at or after this index
44
+ let i = 0;
45
+ while (i < n) {
46
+ if (!TOKEN_CHAR.test(value[i]!)) { i++; continue; }
47
+ while (i < n && TOKEN_CHAR.test(value[i]!)) i++;
48
+ let j = i;
49
+ while (j < n && (value[j] === " " || value[j] === "\t")) j++;
50
+ if (value[j] !== "=") { i = j; continue; }
51
+ j++;
52
+ while (j < n && (value[j] === " " || value[j] === "\t")) j++;
53
+ if (value[j] === "\"") {
54
+ let k = j + 1;
55
+ let text = "";
56
+ let closed = false;
57
+ if (j < unclosedFrom) {
58
+ while (k < n) {
59
+ const char = value[k]!;
60
+ if (char === "\\" && k + 1 < n) { text += value[k + 1]; k += 2; continue; }
61
+ if (char === "\"") { closed = true; break; }
62
+ text += char;
63
+ k++;
64
+ }
65
+ }
66
+ if (closed) {
67
+ if (text) out.push(text);
68
+ i = k + 1;
69
+ } else {
70
+ unclosedFrom = Math.min(unclosedFrom, j);
71
+ i = j + 1;
72
+ }
73
+ continue;
74
+ }
75
+ const start = j;
76
+ while (j < n && !/[\s,;"]/.test(value[j]!)) j++;
77
+ const raw = value.slice(start, j);
78
+ if (raw && !/^=+$/.test(raw)) out.push(raw);
79
+ i = j;
80
+ }
81
+ return out;
82
+ }
83
+
84
+ /**
85
+ * The whole value plus every part a server could authenticate with on its own: tokens split on
86
+ * whitespace, commas and semicolons with auth scheme words dropped, the value of each name=value
87
+ * parameter (cookies, digest; whitespace around "=" and quoted strings with escapes are parsed
88
+ * first, before any splitting), and both halves of a standard Basic credential.
89
+ */
90
+ export function credentialComponents(value: string): string[] {
91
+ const whole = value.trim();
92
+ if (!whole) return [];
93
+ const out = new Set([whole]);
94
+ for (const param of parameterValues(whole)) out.add(param);
95
+ let previous: string | undefined;
96
+ for (const part of whole.split(/[\s,;]+/)) {
97
+ if (!part) continue;
98
+ const lower = part.toLowerCase();
99
+ if (AUTH_SCHEMES.has(lower)) { previous = lower; continue; }
100
+ out.add(part);
101
+ if (previous === "basic" && BASIC_BASE64.test(part)) {
102
+ const decoded = Buffer.from(part, "base64").toString("utf8");
103
+ if (PRINTABLE_USER_PASS.test(decoded) && Buffer.from(decoded, "utf8").toString("base64").replace(/=+$/, "") === part.replace(/=+$/, "")) {
104
+ const colon = decoded.indexOf(":");
105
+ out.add(decoded.slice(0, colon));
106
+ out.add(decoded.slice(colon + 1));
107
+ }
108
+ }
109
+ previous = undefined;
110
+ }
111
+ out.delete("");
112
+ return [...out];
113
+ }
114
+
115
+ /** Exact equality at any length at or above minLength; containment for values of 8+ characters. */
116
+ export function credentialSetMatches(values: Iterable<string>, has: (value: string) => boolean, value: string, minLength = 1): boolean {
117
+ if (value.length >= minLength && has(value)) return true;
118
+ for (const secret of values) {
119
+ if (secret.length >= Math.max(minLength, CREDENTIAL_SUBSTRING_MIN_LENGTH) && value.includes(secret)) return true;
120
+ }
121
+ return false;
122
+ }
123
+
124
+ export interface OutboundCredentialRegistryOptions {
125
+ maxEntries?: number;
126
+ maxBytes?: number;
127
+ maxValueBytes?: number;
128
+ }
129
+
130
+ /** Bounded LRU of values; eviction marks it incomplete. */
131
+ class BoundedValueSet {
132
+ readonly values = new Map<string, number>();
133
+ private totalBytes = 0;
134
+ complete = true;
135
+ constructor(private readonly maxEntries: number, private readonly maxBytes: number, private readonly maxValueBytes: number) {}
136
+
137
+ add(value: string): void {
138
+ if (!value) return;
139
+ const bytes = Buffer.byteLength(value, "utf8");
140
+ if (bytes > this.maxValueBytes) {
141
+ this.complete = false;
142
+ return;
143
+ }
144
+ if (this.values.has(value)) {
145
+ this.values.delete(value);
146
+ this.values.set(value, bytes);
147
+ return;
148
+ }
149
+ this.values.set(value, bytes);
150
+ this.totalBytes += bytes;
151
+ while (this.values.size > this.maxEntries || this.totalBytes > this.maxBytes) {
152
+ const oldest = this.values.keys().next().value as string;
153
+ this.totalBytes -= this.values.get(oldest)!;
154
+ this.values.delete(oldest);
155
+ this.complete = false;
156
+ }
157
+ }
158
+
159
+ matches(value: string, minLength: number): boolean {
160
+ return credentialSetMatches(this.values.keys(), candidate => this.values.has(candidate), value, minLength);
161
+ }
162
+ }
163
+
164
+ export class OutboundCredentialRegistry {
165
+ private readonly strict: BoundedValueSet;
166
+ private readonly observed: BoundedValueSet;
167
+
168
+ constructor(options: OutboundCredentialRegistryOptions = {}) {
169
+ const entries = options.maxEntries ?? 4096;
170
+ const bytes = options.maxBytes ?? 1 << 20;
171
+ const valueBytes = options.maxValueBytes ?? 16_384;
172
+ this.strict = new BoundedValueSet(entries, bytes, valueBytes);
173
+ this.observed = new BoundedValueSet(entries, bytes, valueBytes);
174
+ }
175
+
176
+ /** Record the components of every value in headers that are about to be sent. */
177
+ remember(headers: HeadersInit | Headers | undefined): void {
178
+ if (!headers) return;
179
+ new Headers(headers).forEach((value, name) => {
180
+ const strict = isCredentialName(name);
181
+ if (!strict && CORRELATION_HEADER_NAME.test(name)) return;
182
+ const tier = strict ? this.strict : this.observed;
183
+ for (const part of credentialComponents(value)) tier.add(part);
184
+ });
185
+ }
186
+
187
+ /** True when value repeats a recorded credential (strict at any length, best effort at 8+). */
188
+ matches(value: string): boolean {
189
+ return this.strict.matches(value, 1) || this.observed.matches(value, CREDENTIAL_SUBSTRING_MIN_LENGTH);
190
+ }
191
+
192
+ /** False once a credential-named value was dropped; callers must then fail closed. */
193
+ complete(): boolean {
194
+ return this.strict.complete;
195
+ }
196
+ }
197
+
198
+ let active = new OutboundCredentialRegistry();
199
+
200
+ /** The registry fed by the HTTP executor boundary and the Codex WebSocket dial. */
201
+ export function outboundCredentials(): OutboundCredentialRegistry {
202
+ return active;
203
+ }
204
+
205
+ /** Swap the active registry for sequential in-file tests; returns the restore function. */
206
+ export function setOutboundCredentialRegistryForTests(registry: OutboundCredentialRegistry): () => void {
207
+ const previous = active;
208
+ active = registry;
209
+ return () => { active = previous; };
210
+ }
@@ -68,7 +68,7 @@ export interface PackageTreeIntegrityOptions {
68
68
  }
69
69
 
70
70
  export type ObservePackageTree = () => PackageTreeObservation | null;
71
- export type PackageTreeRuntimeInstall = "bun" | "mise" | "npm" | "pnpm" | "source";
71
+ export type PackageTreeRuntimeInstall = "bun" | "mise" | "npm" | "pnpm" | "source" | "standalone";
72
72
 
73
73
  const packageManifestUrl = new URL("../../package.json", import.meta.url);
74
74
 
@@ -323,7 +323,7 @@ export function createRuntimePackageTreeIntegrityGuard(
323
323
  now: () => number = Date.now,
324
324
  options: PackageTreeIntegrityOptions = {},
325
325
  ): PackageTreeIntegrityGuard {
326
- if (installer === "source" || isStandaloneBinary()) {
326
+ if (installer === "source" || installer === "standalone" || isStandaloneBinary()) {
327
327
  return { status: () => ({ ok: true }), dispose: () => {} };
328
328
  }
329
329
  // The restart replacement spawns process.execPath, the bundled Bun in an npm install. Read it
@@ -0,0 +1,35 @@
1
+ /**
2
+ * A passive management read: it shows what this process already knows and does not make it act on
3
+ * providers, accounts, processes or files (#6959).
4
+ *
5
+ * The Remote Link management relay runs every Child read inside this scope
6
+ * (`src/server/index/link-management.ts`), so a Child can show its Home's state without changing
7
+ * it or making it act. Inside the scope:
8
+ *
9
+ * - model discovery yields no routed models (`fetchAllModels`, the management model rows): it would
10
+ * reach provider upstreams, refresh Codex entitlements and persist a pending provider's initial
11
+ * model selection;
12
+ * - provider quota and the Codex account list are served from what the Home has observed, with no
13
+ * probe, token refresh, native-main claim or publication into routing evidence;
14
+ * - the startup-health diagnostic, the Codex runtime version probes and the bundled-catalog refresh
15
+ * are not started, so no process runs and no runtime record is written;
16
+ * - a native-main fence raised for unknown service ownership is reported, not re-inspected;
17
+ * - config-directory and credential-file hardening and malformed-file backups are skipped;
18
+ * - memory-budget enforcement demotes only stores registered as `passiveEvictable` (eviction that
19
+ * only releases memory: the usage caches), never a continuation spill or a replay snapshot.
20
+ *
21
+ * The Home's own dashboard and traffic keep those caches current. A passive read may still join
22
+ * work the Home already started; that work keeps the Home's scope and its writes are the Home's.
23
+ */
24
+ import { AsyncLocalStorage } from "node:async_hooks";
25
+
26
+ const passive = new AsyncLocalStorage<true>();
27
+
28
+ /** Run a management read passively, including everything it awaits or schedules. */
29
+ export function runPassiveRead<T>(run: () => Promise<T>): Promise<T> {
30
+ return passive.run(true, run);
31
+ }
32
+
33
+ export function isPassiveRead(): boolean {
34
+ return passive.getStore() === true;
35
+ }
@@ -81,19 +81,58 @@ export function shadowCallTargetsIntersect(
81
81
  && target.modelId.startsWith(source.modelId);
82
82
  }
83
83
 
84
+ /**
85
+ * Preserve explicit user turns and spawned children before either intercept site.
86
+ * HTTP provenance is `x-codex-turn-metadata` or `x-openai-subagent: collab_spawn`.
87
+ * `x-openai-subagent` is connection-level on both transports: a child session opens
88
+ * its own connection, so a `collab_spawn` upgrade header still names every frame.
89
+ * Turn metadata is per-frame on WebSocket (`CODEX_WS_FRAME_HEADERS`): only the frame's
90
+ * `client_metadata["x-codex-turn-metadata"]` describes this turn, because the
91
+ * upgrade's copy can describe an earlier user or helper turn.
92
+ * `request_kind: "turn"` alone is not user provenance (#1684). Maintenance
93
+ * markers such as `x-openai-subagent: compact`/`review` and metadata naming
94
+ * `memory_consolidation` remain eligible helpers, subject to memory routing.
95
+ * Missing/malformed metadata retains the legacy opt-in source-prefix behavior.
96
+ */
97
+ export function shadowCallPreservesSelectedModel(
98
+ body: unknown,
99
+ headers: Headers,
100
+ transport?: "websocket",
101
+ ): boolean {
102
+ if (headers.get("x-openai-subagent") === "collab_spawn") return true;
103
+ let metadata: unknown = headers.get("x-codex-turn-metadata");
104
+ if (transport === "websocket") {
105
+ const client = body && typeof body === "object" && !Array.isArray(body)
106
+ ? (body as Record<string, unknown>).client_metadata : undefined;
107
+ metadata = client && typeof client === "object" && !Array.isArray(client)
108
+ && Object.hasOwn(client, "x-codex-turn-metadata")
109
+ ? (client as Record<string, unknown>)["x-codex-turn-metadata"] : undefined;
110
+ }
111
+ if (typeof metadata !== "string") return false;
112
+ try {
113
+ const parsed: unknown = JSON.parse(metadata);
114
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false;
115
+ const turn = parsed as Record<string, unknown>;
116
+ return turn.subagent_kind === "thread_spawn"
117
+ || (turn.request_kind === "turn" && turn.thread_source === "user");
118
+ } catch {
119
+ return false;
120
+ }
121
+ }
122
+
84
123
  /**
85
124
  * Decide whether a matching source model should use the opt-in intercept.
86
125
  *
87
126
  * Before Codex 0.147.0 this checked x-codex-turn-metadata and exempted
88
127
  * request_kind "turn". Codex 0.147.0 can label background helper calls as
89
- * "turn", causing them to bypass the intercept (#1684). The fix is to
90
- * intercept every configured shadow source model regardless of request kind.
128
+ * "turn", causing them to bypass the intercept (#1684). Request kind alone
129
+ * cannot exempt a configured source model: callers first preserve explicit
130
+ * user/child provenance with `shadowCallPreservesSelectedModel` (#6921).
91
131
  * A replacement intersecting the same provider+model source set remains a
92
132
  * no-op because rewriting it would only create self-interception (#2706).
93
133
  *
94
- * Callers skip this check entirely for spawned sub-agent turns
95
- * (`isThreadSpawnRequest`): `gpt-6-luna` is both the helper slug and a default
96
- * sub-agent model, and an explicitly spawned child must keep the model it chose.
134
+ * `gpt-6-luna` is both the helper slug and a selectable user/sub-agent model;
135
+ * explicit user turns and spawned children must keep the model they chose.
97
136
  */
98
137
  export function shouldInterceptShadowCall(
99
138
  modelId: string,
@@ -1,6 +1,7 @@
1
1
  import net, { type Socket } from "node:net";
2
2
  import tls, { type TLSSocket } from "node:tls";
3
3
  import { classifyContentCoding, isNullBodyStatus } from "./http-response-semantics";
4
+ import { socks5Credentials, socks5Handshake, Socks5HandshakeError } from "./socks5-handshake";
4
5
 
5
6
  const DEFAULT_SOCKS5_PORT = 1080;
6
7
  const SOCKS5_CONNECT_TIMEOUT_MS = 30_000;
@@ -10,11 +11,6 @@ const MAX_BODY_SLICE_BYTES = 64 * 1024;
10
11
  const MAX_DECODED_BODY_BYTES = 32 * 1024 * 1024;
11
12
  const MAX_STREAM_DECODE_EXPANSION_RATIO = 128;
12
13
  const SOCKS5_VERSION = 0x05;
13
- const SOCKS5_NO_AUTH = 0x00;
14
- const SOCKS5_USER_PASS = 0x02;
15
- const SOCKS5_CONNECT = 0x01;
16
- const SOCKS5_DOMAIN = 0x03;
17
- const SOCKS5_SUCCESS = 0x00;
18
14
  const CRLF = Buffer.from("\r\n");
19
15
  const HEADER_END = Buffer.from("\r\n\r\n");
20
16
 
@@ -22,24 +18,6 @@ export class Socks5FetchError extends Error {
22
18
  override readonly name = "Socks5FetchError";
23
19
  }
24
20
 
25
- function proxyCredentials(proxy: URL): { username?: Uint8Array; password?: Uint8Array } {
26
- if (!proxy.username && !proxy.password) return {};
27
- let username: string;
28
- let password: string;
29
- try {
30
- username = decodeURIComponent(proxy.username);
31
- password = decodeURIComponent(proxy.password);
32
- } catch {
33
- throw new Socks5FetchError("SOCKS5 proxy credentials contain invalid percent encoding");
34
- }
35
- const usernameBytes = new TextEncoder().encode(username);
36
- const passwordBytes = new TextEncoder().encode(password);
37
- if (usernameBytes.byteLength > 255 || passwordBytes.byteLength > 255) {
38
- throw new Socks5FetchError("SOCKS5 proxy credentials must each fit in 255 UTF-8 bytes");
39
- }
40
- return { username: usernameBytes, password: passwordBytes };
41
- }
42
-
43
21
  function validateProxy(proxy: string): URL {
44
22
  let parsed: URL;
45
23
  try {
@@ -55,7 +33,10 @@ function validateProxy(proxy: string): URL {
55
33
  throw new Socks5FetchError("SOCKS5 proxy port is invalid");
56
34
  }
57
35
  if (parsed.search || parsed.hash) throw new Socks5FetchError("SOCKS5 proxy URL must not contain a query or fragment");
58
- proxyCredentials(parsed);
36
+ try { socks5Credentials(parsed); } catch (error) {
37
+ if (error instanceof Socks5HandshakeError) throw new Socks5FetchError(error.message);
38
+ throw error;
39
+ }
59
40
  return parsed;
60
41
  }
61
42
 
@@ -130,6 +111,14 @@ class SocketReader {
130
111
  socket.resume();
131
112
  }
132
113
 
114
+ write(bytes: Uint8Array): void {
115
+ this.socket.write(bytes);
116
+ }
117
+
118
+ readExact(bytes: number, signal?: AbortSignal): Promise<Buffer> {
119
+ return this.read(bytes, signal);
120
+ }
121
+
133
122
  private readonly onData = (chunk: Buffer | string): void => {
134
123
  const value = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
135
124
  if (this.anyWaiters.length > 0) {
@@ -263,7 +252,7 @@ class SocketReader {
263
252
 
264
253
  async function socks5Connect(proxy: string, target: URL, signal?: AbortSignal): Promise<Socket> {
265
254
  const parsedProxy = validateProxy(proxy);
266
- const credentials = proxyCredentials(parsedProxy);
255
+ const credentials = socks5Credentials(parsedProxy);
267
256
  const proxyHost = parsedProxy.hostname.replace(/^\[|\]$/g, "");
268
257
  const socket = await connectSocket(proxyHost, Number(parsedProxy.port) || DEFAULT_SOCKS5_PORT, signal);
269
258
  socket.setTimeout(SOCKS5_CONNECT_TIMEOUT_MS, () => {
@@ -271,49 +260,12 @@ async function socks5Connect(proxy: string, target: URL, signal?: AbortSignal):
271
260
  });
272
261
  const reader = new SocketReader(socket);
273
262
  try {
274
- const methods = credentials.username ? Buffer.from([SOCKS5_NO_AUTH, SOCKS5_USER_PASS]) : Buffer.from([SOCKS5_NO_AUTH]);
275
- socket.write(Buffer.from([SOCKS5_VERSION, methods.byteLength, ...methods]));
276
- const greeting = await reader.read(2, signal);
277
- if (greeting[0] !== SOCKS5_VERSION) throw new Socks5FetchError("SOCKS5 proxy returned an invalid greeting");
278
- if (greeting[1] === SOCKS5_USER_PASS && credentials.username && credentials.password) {
279
- socket.write(Buffer.from([
280
- 0x01,
281
- credentials.username.byteLength,
282
- ...credentials.username,
283
- credentials.password.byteLength,
284
- ...credentials.password,
285
- ]));
286
- const auth = await reader.read(2, signal);
287
- if (auth[0] !== 0x01 || auth[1] !== 0x00) throw new Socks5FetchError("SOCKS5 proxy authentication failed");
288
- } else if (greeting[1] !== SOCKS5_NO_AUTH) {
289
- throw new Socks5FetchError("SOCKS5 proxy does not accept an offered authentication method");
290
- }
291
-
292
- const hostname = new TextEncoder().encode(target.hostname);
293
- if (hostname.byteLength > 255) throw new Socks5FetchError("SOCKS5 target hostname is too long");
294
- const port = targetPort(target);
295
- socket.write(Buffer.from([
296
- SOCKS5_VERSION,
297
- SOCKS5_CONNECT,
298
- 0x00,
299
- SOCKS5_DOMAIN,
300
- hostname.byteLength,
301
- ...hostname,
302
- port >> 8,
303
- port & 0xff,
304
- ]));
305
- const reply = await reader.read(4, signal);
306
- if (reply[0] !== SOCKS5_VERSION) throw new Socks5FetchError("SOCKS5 proxy returned an invalid connect response");
307
- if (reply[1] !== SOCKS5_SUCCESS) throw new Socks5FetchError(`SOCKS5 proxy refused the connection (code ${reply[1]})`);
308
- if (reply[2] !== 0x00 || ![0x01, SOCKS5_DOMAIN, 0x04].includes(reply[3]!)) {
309
- throw new Socks5FetchError("SOCKS5 proxy returned an invalid address type or reserved byte");
310
- }
311
- const addressLength = reply[3] === 0x01 ? 4 : reply[3] === SOCKS5_DOMAIN ? (await reader.read(1, signal))[0]! : 16;
312
- await reader.read(addressLength + 2, signal);
263
+ await socks5Handshake(reader, { host: target.hostname, port: targetPort(target) }, credentials, signal);
313
264
  socket.setTimeout(0);
314
265
  return socket;
315
266
  } catch (error) {
316
267
  socket.destroy();
268
+ if (error instanceof Socks5HandshakeError) throw new Socks5FetchError(error.message);
317
269
  throw error;
318
270
  } finally {
319
271
  reader.dispose();
@@ -341,25 +293,50 @@ function requestHeaders(request: Request, target: URL): { text: string; chunked:
341
293
 
342
294
  async function secureSocket(socket: Socket, target: URL, signal: AbortSignal): Promise<TLSSocket> {
343
295
  return new Promise((resolve, reject) => {
296
+ if (signal.aborted) {
297
+ socket.destroy();
298
+ reject(signal.reason instanceof Error ? signal.reason : new Error("The operation was aborted"));
299
+ return;
300
+ }
344
301
  const tlsSocket = tls.connect({
345
302
  socket,
346
303
  servername: target.hostname,
347
304
  rejectUnauthorized: true,
348
305
  });
349
- const onAbort = () => {
306
+ let settled = false;
307
+ // Absolute deadline: a peer sending occasional TLS bytes must not extend the handshake.
308
+ const timer = setTimeout(() => {
309
+ fail(new Socks5FetchError("SOCKS5 TLS handshake timed out"));
310
+ }, SOCKS5_CONNECT_TIMEOUT_MS);
311
+ const cleanup = () => {
312
+ clearTimeout(timer);
313
+ signal.removeEventListener("abort", onAbort);
314
+ tlsSocket.removeListener("secureConnect", onSecureConnect);
315
+ tlsSocket.removeListener("error", onError);
316
+ tlsSocket.removeListener("close", onClose);
317
+ };
318
+ const fail = (error: Error) => {
319
+ if (settled) return;
320
+ settled = true;
321
+ cleanup();
322
+ // No error argument: pending event observers must not receive a second error.
350
323
  tlsSocket.destroy();
351
- reject(signal.reason instanceof Error ? signal.reason : new Error("The operation was aborted"));
324
+ reject(error);
352
325
  };
326
+ const onAbort = () => fail(signal.reason instanceof Error ? signal.reason : new Error("The operation was aborted"));
327
+ const onError = (error: Error) => fail(error);
328
+ const onClose = () => fail(new Socks5FetchError("SOCKS5 upstream closed during TLS handshake"));
353
329
  const onSecureConnect = () => {
354
- signal.removeEventListener("abort", onAbort);
330
+ if (settled) return;
331
+ settled = true;
332
+ cleanup();
355
333
  resolve(tlsSocket);
356
334
  };
357
335
  tlsSocket.once("secureConnect", onSecureConnect);
358
- tlsSocket.once("error", error => {
359
- signal.removeEventListener("abort", onAbort);
360
- reject(error);
361
- });
336
+ tlsSocket.once("error", onError);
337
+ tlsSocket.once("close", onClose);
362
338
  signal.addEventListener("abort", onAbort, { once: true });
339
+ if (signal.aborted) onAbort();
363
340
  });
364
341
  }
365
342