@bitkyc08/opencodex 2.84.0 → 2.87.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 (518) hide show
  1. package/README.md +0 -4
  2. package/bin/ocx.mjs +61 -3
  3. package/gui/dist/assets/App-ZfYbLHkB.js +58 -0
  4. package/gui/dist/assets/App-wT3mXqFz.css +1 -0
  5. package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DCd-A1az.js} +1 -1
  6. package/gui/dist/assets/index-CfYDMj34.js +94 -0
  7. package/gui/dist/assets/index-DPeB9HpV.css +1 -0
  8. package/gui/dist/assets/quota-summary-ChvmKGOk.js +1 -0
  9. package/gui/dist/index.html +2 -2
  10. package/package.json +1 -2
  11. package/src/adapters/anthropic/beta-allowlist.ts +91 -11
  12. package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
  13. package/src/adapters/anthropic/client-identity.ts +1 -1
  14. package/src/adapters/anthropic/passthrough.ts +16 -3
  15. package/src/adapters/anthropic-progress.ts +84 -0
  16. package/src/adapters/anthropic.ts +108 -21
  17. package/src/adapters/azure.ts +19 -1
  18. package/src/adapters/base.ts +2 -0
  19. package/src/adapters/claude-cli/adapter.ts +111 -23
  20. package/src/adapters/claude-cli/profiles.ts +2 -1
  21. package/src/adapters/claude-cli/stable-replay.ts +108 -0
  22. package/src/adapters/claude-cli/usage-admission.ts +230 -0
  23. package/src/adapters/client-fingerprint.ts +24 -0
  24. package/src/adapters/coding-agent/protocol.ts +1 -1
  25. package/src/adapters/coding-agent/turn.ts +40 -11
  26. package/src/adapters/cursor/arg-codec.ts +17 -5
  27. package/src/adapters/cursor/catalog.ts +8 -0
  28. package/src/adapters/cursor/effort-map.ts +3 -0
  29. package/src/adapters/cursor/freeform-args.ts +75 -0
  30. package/src/adapters/cursor/protobuf-events.ts +36 -23
  31. package/src/adapters/dsh-account.ts +97 -0
  32. package/src/adapters/google-antigravity-wire.ts +22 -1
  33. package/src/adapters/google.ts +37 -10
  34. package/src/adapters/openai-chat/messages.ts +4 -3
  35. package/src/adapters/openai-chat/passthrough.ts +8 -2
  36. package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
  37. package/src/adapters/openai-chat/response-events.ts +59 -1
  38. package/src/adapters/openai-chat.ts +8 -5
  39. package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
  40. package/src/adapters/openai-responses/passthrough.ts +57 -125
  41. package/src/adapters/openai-responses/request-strips.ts +25 -3
  42. package/src/adapters/openai-responses/transport.ts +81 -0
  43. package/src/adapters/qoder/adapter.ts +44 -3
  44. package/src/adapters/registry.ts +5 -0
  45. package/src/adapters/zed.ts +26 -1
  46. package/src/advisor/activity.ts +55 -0
  47. package/src/advisor/budget.ts +62 -0
  48. package/src/advisor/consult.ts +223 -0
  49. package/src/advisor/context.ts +238 -0
  50. package/src/advisor/disclosure.ts +20 -0
  51. package/src/advisor/evidence-tool.ts +21 -0
  52. package/src/advisor/evidence.ts +117 -0
  53. package/src/advisor/runtime.ts +353 -0
  54. package/src/advisor/settings.ts +144 -0
  55. package/src/advisor/state.ts +369 -0
  56. package/src/advisor/synthetic-tool.ts +33 -0
  57. package/src/bridge/errors.ts +24 -1
  58. package/src/bridge/internal.ts +3 -1
  59. package/src/bridge/response-json.ts +18 -3
  60. package/src/bridge/sse.ts +58 -8
  61. package/src/bridge/terminal-refusal.ts +59 -0
  62. package/src/chat/inbound.ts +30 -6
  63. package/src/chat/outbound.ts +5 -0
  64. package/src/claude/desktop-3p-library.ts +72 -7
  65. package/src/claude/desktop-3p.ts +75 -19
  66. package/src/claude/desktop-first-party.ts +1 -1
  67. package/src/claude/desktop-picker-platform.ts +4 -0
  68. package/src/claude/desktop-picker-profile.ts +11 -4
  69. package/src/claude/desktop-picker.ts +15 -8
  70. package/src/claude/desktop-profile.ts +10 -1
  71. package/src/claude/desktop-remote-store-io.ts +5 -3
  72. package/src/claude/desktop-system-proxy.ts +50 -4
  73. package/src/claude/inbound-content-options.ts +11 -0
  74. package/src/claude/inbound.ts +154 -50
  75. package/src/claude/intercept/local-ca.ts +8 -1
  76. package/src/claude/intercept/picker-ca-startup.ts +2 -1
  77. package/src/claude/intercept/picker-runtime.ts +6 -5
  78. package/src/claude/intercept/picker-trust-windows.ts +338 -0
  79. package/src/claude/intercept/picker-trust.ts +19 -0
  80. package/src/claude/intercept/port-policy.ts +30 -0
  81. package/src/claude/intercept/runtime.ts +97 -94
  82. package/src/claude/model-default-settings.ts +154 -0
  83. package/src/claude/model-info.ts +7 -7
  84. package/src/claude/outbound.ts +51 -19
  85. package/src/cli/account-api.ts +1 -0
  86. package/src/cli/account-policy-dto.ts +6 -0
  87. package/src/cli/account-policy.ts +34 -5
  88. package/src/cli/account-preservation.ts +44 -0
  89. package/src/cli/account.ts +8 -2
  90. package/src/cli/advisor.ts +165 -0
  91. package/src/cli/auxiliary-listener-preflight.ts +37 -0
  92. package/src/cli/capabilities-accounts.ts +20 -1
  93. package/src/cli/capabilities-agents-routing.ts +23 -2
  94. package/src/cli/capabilities-base.ts +39 -11
  95. package/src/cli/capabilities-observe-system.ts +63 -3
  96. package/src/cli/capabilities-provider-models.ts +11 -0
  97. package/src/cli/chatgpt-command.ts +35 -14
  98. package/src/cli/claude.ts +46 -6
  99. package/src/cli/codex-cli-update.ts +151 -41
  100. package/src/cli/codex-provider.ts +44 -0
  101. package/src/cli/combo-input.ts +10 -1
  102. package/src/cli/combo-result.ts +1 -1
  103. package/src/cli/combo.ts +2 -1
  104. package/src/cli/cross-home-owner.ts +17 -14
  105. package/src/cli/dispatch.ts +30 -5
  106. package/src/cli/doctor.ts +15 -1
  107. package/src/cli/ensure-desktop-cold-start.ts +382 -0
  108. package/src/cli/export-command.ts +1 -1
  109. package/src/cli/help.ts +4 -2
  110. package/src/cli/index.ts +14 -5
  111. package/src/cli/integration-plan-dto.ts +3 -0
  112. package/src/cli/log-history.ts +172 -0
  113. package/src/cli/models-runtime-subcommands.ts +1 -0
  114. package/src/cli/models-runtime.ts +72 -8
  115. package/src/cli/observe-stream.ts +2 -0
  116. package/src/cli/observe.ts +16 -4
  117. package/src/cli/provider.ts +27 -5
  118. package/src/cli/registry.ts +41 -9
  119. package/src/cli/restart-scope.ts +3 -1
  120. package/src/cli/runtime-api.ts +5 -3
  121. package/src/cli/system-command.ts +40 -8
  122. package/src/cli/uninstall-client-state.ts +32 -0
  123. package/src/cli/usage-compare.ts +309 -0
  124. package/src/cli/usage-model-search.ts +4 -4
  125. package/src/cli/usage-report.ts +2 -2
  126. package/src/cli/v2.ts +1 -1
  127. package/src/client/machine-listener.ts +1 -3
  128. package/src/client/runtime.ts +1 -0
  129. package/src/clients/client-path.ts +51 -0
  130. package/src/clients/config-export/contracts.ts +3 -1
  131. package/src/clients/config-export/model-metadata.ts +3 -2
  132. package/src/clients/config-export/qoder.ts +93 -0
  133. package/src/clients/config-export.ts +35 -48
  134. package/src/clients/gentle-shell-path.ts +87 -0
  135. package/src/codex/account-auto-switch.ts +2 -0
  136. package/src/codex/account-identity.ts +29 -0
  137. package/src/codex/account-label.ts +1 -1
  138. package/src/codex/account-lifecycle.ts +21 -5
  139. package/src/codex/account-preservation.ts +28 -0
  140. package/src/codex/account-use-remaining.ts +23 -0
  141. package/src/codex/app-server-processes.ts +97 -7
  142. package/src/codex/app-server-restart-service.ts +55 -4
  143. package/src/codex/auth-api/account-list.ts +4 -0
  144. package/src/codex/auth-api/account-pause-group.ts +6 -50
  145. package/src/codex/auth-api/account-preservation.ts +57 -0
  146. package/src/codex/auth-api/pool-mode-gate.ts +5 -2
  147. package/src/codex/auth-api/routes.ts +8 -0
  148. package/src/codex/auth-api/use-remaining-route.ts +36 -0
  149. package/src/codex/auth-api.ts +0 -1
  150. package/src/codex/auth-context.ts +81 -7
  151. package/src/codex/autostart-health.ts +4 -0
  152. package/src/codex/catalog/auto-review.ts +20 -3
  153. package/src/codex/catalog/build-entries.ts +19 -2
  154. package/src/codex/catalog/combo-member.ts +14 -5
  155. package/src/codex/catalog/configured-tiers.ts +50 -0
  156. package/src/codex/catalog/effort.ts +4 -1
  157. package/src/codex/catalog/gather-capture.ts +2 -0
  158. package/src/codex/catalog/metadata.ts +11 -9
  159. package/src/codex/catalog/model-hints.ts +42 -19
  160. package/src/codex/catalog/model-visibility.ts +3 -1
  161. package/src/codex/catalog/native-ultrafast.ts +106 -0
  162. package/src/codex/catalog/parsing.ts +35 -86
  163. package/src/codex/catalog/provider-models.ts +40 -34
  164. package/src/codex/catalog/retained-sync.ts +2 -0
  165. package/src/codex/catalog/routed-gather.ts +33 -16
  166. package/src/codex/catalog/vendor-context.ts +82 -0
  167. package/src/codex/cli-install-provenance.ts +105 -17
  168. package/src/codex/cli-update-plan.ts +545 -0
  169. package/src/codex/config-write-lock.ts +63 -4
  170. package/src/codex/convergence.ts +2 -0
  171. package/src/codex/desktop-app/picker-freshness.ts +96 -0
  172. package/src/codex/desktop-app/types.ts +2 -0
  173. package/src/codex/desktop-app/windows.ts +29 -16
  174. package/src/codex/desktop-app-restart.ts +25 -2
  175. package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
  176. package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
  177. package/src/codex/desktop-compatibility/connection-store.ts +87 -0
  178. package/src/codex/desktop-compatibility/installed-build.ts +73 -0
  179. package/src/codex/desktop-compatibility/json-body.ts +29 -0
  180. package/src/codex/desktop-compatibility/native-identity.ts +62 -0
  181. package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
  182. package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
  183. package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
  184. package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
  185. package/src/codex/desktop-compatibility/runtime.ts +182 -0
  186. package/src/codex/desktop-compatibility/service.ts +30 -0
  187. package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
  188. package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
  189. package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
  190. package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
  191. package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
  192. package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
  193. package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
  194. package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
  195. package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
  196. package/src/codex/desktop-compatibility/windows-key-protection.ts +165 -0
  197. package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
  198. package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
  199. package/src/codex/features.ts +85 -45
  200. package/src/codex/history-provider.ts +1 -1
  201. package/src/codex/inject/config-write-section.ts +4 -1
  202. package/src/codex/inject/history-failure.ts +27 -0
  203. package/src/codex/inject/remove.ts +18 -6
  204. package/src/codex/inject/restore.ts +24 -2
  205. package/src/codex/inject/routing-classify.ts +4 -3
  206. package/src/codex/inject.ts +18 -27
  207. package/src/codex/journal.ts +16 -1
  208. package/src/codex/main-account-hard-lock.ts +30 -1
  209. package/src/codex/main-account.ts +99 -15
  210. package/src/codex/native-main-refresh-mode.ts +217 -0
  211. package/src/codex/native-profile-startup.ts +1 -1
  212. package/src/codex/ocx-compaction-history.ts +12 -10
  213. package/src/codex/paths.ts +86 -1
  214. package/src/codex/provider-routing.ts +174 -0
  215. package/src/codex/refresh.ts +2 -2
  216. package/src/codex/routing/health-store.ts +2 -2
  217. package/src/codex/routing/pool-recovery.ts +79 -0
  218. package/src/codex/routing/selection.ts +4 -3
  219. package/src/codex/routing/thread-affinity.ts +1 -1
  220. package/src/codex/shim-diagnostics.ts +6 -0
  221. package/src/codex/shim-overlay.ts +7 -2
  222. package/src/codex/signed-in-identity.ts +43 -0
  223. package/src/codex/subagent-model-fallback.ts +1 -1
  224. package/src/codex/sync.ts +6 -1
  225. package/src/codex/user-identity.ts +16 -5
  226. package/src/codex/windows-local-appdata.ts +84 -0
  227. package/src/combos/failover.ts +6 -1
  228. package/src/combos/index.ts +1 -0
  229. package/src/combos/resolve.ts +46 -10
  230. package/src/combos/types.ts +15 -0
  231. package/src/config/diagnostics.ts +35 -7
  232. package/src/config/live-reconcile.ts +51 -2
  233. package/src/config/load-degrade.ts +23 -1
  234. package/src/config/opencode-wire-upgrade.ts +54 -0
  235. package/src/config/opencode-zen-upgrade.ts +52 -0
  236. package/src/config/owner-registry.ts +22 -17
  237. package/src/config/paths.ts +10 -3
  238. package/src/config/provider-validation.ts +45 -2
  239. package/src/config/schema/config-schema.ts +39 -0
  240. package/src/config/schema/desktop-compatibility.ts +9 -0
  241. package/src/config/schema/leaf-validators.ts +39 -9
  242. package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
  243. package/src/config/schema/native-reasoning-retention.ts +26 -0
  244. package/src/config.ts +2 -0
  245. package/src/generated/compatibility-version.json +962 -350
  246. package/src/generated/model-metadata.ts +1 -1
  247. package/src/grok/sync.ts +1 -1
  248. package/src/images/loop.ts +46 -8
  249. package/src/integrations/catalog-refresh.ts +1 -1
  250. package/src/integrations/config-io.ts +20 -2
  251. package/src/integrations/journal.ts +15 -1
  252. package/src/integrations/mutation-plan.ts +2 -0
  253. package/src/integrations/registry.ts +15 -0
  254. package/src/integrations/store.ts +3 -0
  255. package/src/integrations/writer.ts +10 -1
  256. package/src/lib/advisor-activation.ts +16 -0
  257. package/src/lib/background-work.ts +65 -0
  258. package/src/lib/bounded-file-read.ts +58 -0
  259. package/src/lib/bun-path-runtime.d.mts +10 -1
  260. package/src/lib/bun-path-runtime.mjs +62 -3
  261. package/src/lib/codex-restart-contract.ts +15 -0
  262. package/src/lib/desktop-proxy-route.ts +30 -0
  263. package/src/lib/desktop-upstream-tunnel.ts +72 -0
  264. package/src/lib/empty-tool-catalog.ts +62 -0
  265. package/src/lib/errors.ts +26 -3
  266. package/src/lib/gcp-adc.ts +96 -9
  267. package/src/lib/gcp-credential-input.ts +12 -0
  268. package/src/lib/lab-live-route-production.ts +5 -0
  269. package/src/lib/local-internal-call-capability.ts +55 -0
  270. package/src/lib/opaque-proxy-tunnel.ts +217 -0
  271. package/src/lib/package-tree-integrity.ts +2 -2
  272. package/src/lib/redact.ts +2 -2
  273. package/src/lib/socks5-fetch.ts +48 -71
  274. package/src/lib/socks5-handshake.ts +112 -0
  275. package/src/lib/translator-budget.ts +7 -1
  276. package/src/oauth/account-quota-rank.ts +69 -2
  277. package/src/oauth/anthropic-account-refusal.ts +20 -2
  278. package/src/oauth/anthropic-oauth-definitions.ts +3 -1
  279. package/src/oauth/anthropic-routing.ts +1 -21
  280. package/src/oauth/anthropic.ts +38 -7
  281. package/src/oauth/callback-server.ts +27 -1
  282. package/src/oauth/chatgpt-device.ts +1 -1
  283. package/src/oauth/chatgpt-shared.ts +83 -0
  284. package/src/oauth/chatgpt.ts +8 -72
  285. package/src/oauth/dsh.ts +203 -0
  286. package/src/oauth/generic-account-failover.ts +62 -12
  287. package/src/oauth/generic-session-affinity.ts +125 -0
  288. package/src/oauth/index.ts +188 -111
  289. package/src/oauth/kiro-device-login.ts +2 -1
  290. package/src/oauth/kiro.ts +7 -1
  291. package/src/oauth/local-token-detect.ts +21 -1
  292. package/src/oauth/login-cli-errors.ts +44 -0
  293. package/src/oauth/login-cli.ts +69 -7
  294. package/src/oauth/login-flow-state.ts +371 -23
  295. package/src/oauth/meta-muse-errors.ts +41 -0
  296. package/src/oauth/meta-muse-keychain.ts +52 -0
  297. package/src/oauth/meta-muse.ts +51 -84
  298. package/src/oauth/model-discovery-auth.ts +3 -0
  299. package/src/oauth/pool-settings-capability.ts +4 -2
  300. package/src/oauth/refresh-flight-retirement.ts +48 -0
  301. package/src/oauth/session-affinity-key.ts +23 -0
  302. package/src/oauth/store-anthropic-instance.ts +9 -2
  303. package/src/oauth/store.ts +12 -5
  304. package/src/protocols/codecs/chat-ir.ts +467 -0
  305. package/src/protocols/codecs/chat.ts +10 -1
  306. package/src/protocols/codecs/messages-ir.ts +500 -0
  307. package/src/protocols/encoders/adapter-events.ts +5 -4
  308. package/src/protocols/encoders/messages.ts +9 -6
  309. package/src/protocols/features.ts +10 -2
  310. package/src/protocols/path.ts +11 -1
  311. package/src/protocols/plan-snapshot.ts +39 -16
  312. package/src/protocols/plan.ts +9 -1
  313. package/src/protocols/provider-summary.ts +2 -1
  314. package/src/protocols/settings.ts +8 -0
  315. package/src/providers/antigravity-account-roster.ts +102 -0
  316. package/src/providers/antigravity-models.ts +85 -7
  317. package/src/providers/api-key-cooldowns.ts +49 -0
  318. package/src/providers/api-key-resolve.ts +11 -6
  319. package/src/providers/api-key-response-ownership.ts +57 -0
  320. package/src/providers/api-key-selection.ts +31 -6
  321. package/src/providers/api-keys.ts +214 -3
  322. package/src/providers/claude-cli-usage.ts +96 -0
  323. package/src/providers/dsh-account-destination.ts +31 -0
  324. package/src/providers/dsh-account-models.ts +41 -0
  325. package/src/providers/gcp-credential-cleanup.ts +59 -0
  326. package/src/providers/key-failover.ts +54 -50
  327. package/src/providers/key-store.ts +7 -2
  328. package/src/providers/model-rename-fields.ts +5 -0
  329. package/src/providers/model-rename-migration.ts +7 -5
  330. package/src/providers/openai-tiers-destination.ts +11 -2
  331. package/src/providers/openai-tiers.ts +5 -12
  332. package/src/providers/opencode-go-transport.ts +3 -1
  333. package/src/providers/quota/account-cache.ts +6 -5
  334. package/src/providers/quota/account-row.ts +34 -0
  335. package/src/providers/quota/antigravity.ts +28 -7
  336. package/src/providers/quota/dsh-account.ts +70 -0
  337. package/src/providers/quota/vendor-probes-oauth.ts +1 -93
  338. package/src/providers/quota-types.ts +2 -0
  339. package/src/providers/quota.ts +14 -20
  340. package/src/providers/registry/entries-core.ts +44 -14
  341. package/src/providers/registry/entries-extended.ts +19 -0
  342. package/src/providers/registry/types.ts +1 -1
  343. package/src/providers/request-pacing.ts +77 -19
  344. package/src/providers/routed-config.ts +339 -0
  345. package/src/providers/service-tier.ts +2 -1
  346. package/src/quota/window-mapping.ts +5 -2
  347. package/src/responses/anthropic-progress.ts +34 -0
  348. package/src/responses/assistant-replay.ts +35 -0
  349. package/src/responses/compaction-history-framing.ts +65 -0
  350. package/src/responses/compaction.ts +6 -1
  351. package/src/responses/incomplete-reason.ts +31 -0
  352. package/src/responses/parser.ts +78 -22
  353. package/src/responses/reasoning-envelope.ts +4 -0
  354. package/src/responses/reasoning-replay-cache.ts +24 -2
  355. package/src/responses/reasoning-retention.ts +173 -0
  356. package/src/responses/retained-compaction.ts +46 -0
  357. package/src/responses/schema.ts +23 -0
  358. package/src/responses/state.ts +32 -29
  359. package/src/router.ts +8 -321
  360. package/src/routing/health.ts +37 -22
  361. package/src/routing/history/build-file.ts +92 -0
  362. package/src/routing/history/indexer.ts +254 -221
  363. package/src/routing/history/ingest.ts +242 -0
  364. package/src/routing/history/rebuild-job.ts +111 -0
  365. package/src/routing/history/rebuild-protocol.ts +15 -0
  366. package/src/routing/history/rebuild-worker.ts +34 -0
  367. package/src/routing/profile.ts +1 -1
  368. package/src/server/admission-model-scope.ts +21 -1
  369. package/src/server/antigravity-direct-eligibility.ts +208 -0
  370. package/src/server/auth-cors.ts +15 -0
  371. package/src/server/chat-completions.ts +82 -10
  372. package/src/server/chat-native-reasoning.ts +123 -0
  373. package/src/server/chat-native-sse.ts +110 -7
  374. package/src/server/chat-native.ts +76 -17
  375. package/src/server/claude-messages.ts +164 -433
  376. package/src/server/effort-policy.ts +1 -1
  377. package/src/server/images.ts +15 -2
  378. package/src/server/index/desktop-compatibility-startup.ts +56 -0
  379. package/src/server/index/model-alias-id.ts +36 -0
  380. package/src/server/index/optional-listeners.ts +2 -0
  381. package/src/server/index/serve-options.ts +11 -3
  382. package/src/server/index/startup-warnings.ts +11 -0
  383. package/src/server/index/websocket-handler.ts +9 -0
  384. package/src/server/index.ts +14 -11
  385. package/src/server/inference/advisor-diagnostics.ts +31 -0
  386. package/src/server/inference/client-encoder-delivery.ts +6 -5
  387. package/src/server/login-shell-path.ts +146 -0
  388. package/src/server/management/advisor-routes.ts +202 -0
  389. package/src/server/management/agent-settings-routes.ts +22 -4
  390. package/src/server/management/claude-desktop-picker-routes.ts +1 -1
  391. package/src/server/management/combo-routes.ts +176 -50
  392. package/src/server/management/companion-routes.ts +4 -0
  393. package/src/server/management/config-routes.ts +77 -6
  394. package/src/server/management/context.ts +4 -0
  395. package/src/server/management/cursor-integration-routes.ts +1 -1
  396. package/src/server/management/desktop-compatibility-routes.ts +46 -0
  397. package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
  398. package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
  399. package/src/server/management/model-routes.ts +13 -8
  400. package/src/server/management/model-rows.ts +14 -0
  401. package/src/server/management/native-integration-routes.ts +2 -7
  402. package/src/server/management/native-reasoning-retention-routes.ts +41 -0
  403. package/src/server/management/oauth-account-routes.ts +87 -14
  404. package/src/server/management/protocol-settings-patch.ts +1 -0
  405. package/src/server/management/provider-gcp-credential-routes.ts +29 -0
  406. package/src/server/management/provider-key-pacing-routes.ts +31 -0
  407. package/src/server/management/provider-overwrite-carry.ts +7 -0
  408. package/src/server/management/provider-routes.ts +25 -30
  409. package/src/server/management/route-registry.ts +21 -1
  410. package/src/server/management/sibling-guard.ts +1 -0
  411. package/src/server/management-api.ts +9 -1
  412. package/src/server/messages-antigravity-direct.ts +195 -0
  413. package/src/server/messages-native-tool-guard.ts +185 -0
  414. package/src/server/messages-native.ts +106 -22
  415. package/src/server/messages-shared.ts +431 -0
  416. package/src/server/relay.ts +1 -1
  417. package/src/server/request-log-terminal-status.ts +1 -1
  418. package/src/server/request-log.ts +13 -1
  419. package/src/server/responses/account-change-state.ts +8 -2
  420. package/src/server/responses/adapter-continuation.ts +48 -19
  421. package/src/server/responses/adapter-delivery.ts +43 -8
  422. package/src/server/responses/adapter-dispatch.ts +48 -4
  423. package/src/server/responses/advisor-plan-slot.ts +27 -0
  424. package/src/server/responses/advisor-retention.ts +117 -0
  425. package/src/server/responses/advisor-slot.ts +368 -0
  426. package/src/server/responses/code-mode-collab-surface.ts +116 -8
  427. package/src/server/responses/codex-ws-wire.ts +15 -8
  428. package/src/server/responses/compact.ts +303 -125
  429. package/src/server/responses/compaction-recovery.ts +18 -10
  430. package/src/server/responses/compaction-retention.ts +61 -0
  431. package/src/server/responses/context-overflow.ts +11 -6
  432. package/src/server/responses/core-auth.ts +6 -0
  433. package/src/server/responses/core-codex-account.ts +11 -1
  434. package/src/server/responses/core-combo-failure.ts +12 -1
  435. package/src/server/responses/core-combo.ts +141 -36
  436. package/src/server/responses/core-errors.ts +3 -3
  437. package/src/server/responses/core-lifetime.ts +7 -2
  438. package/src/server/responses/core-normalize.ts +11 -0
  439. package/src/server/responses/core-opaque-recovery.ts +35 -0
  440. package/src/server/responses/core-options.ts +51 -0
  441. package/src/server/responses/core-pipeline.ts +141 -0
  442. package/src/server/responses/core-replay.ts +25 -6
  443. package/src/server/responses/core.ts +3 -116
  444. package/src/server/responses/fetch-helpers.ts +73 -19
  445. package/src/server/responses/main-hard-lock-routing.ts +109 -0
  446. package/src/server/responses/native-response-control.ts +4 -2
  447. package/src/server/responses/passthrough-delivery.ts +19 -11
  448. package/src/server/responses/passthrough-dispatch.ts +29 -16
  449. package/src/server/responses/request-prepare.ts +144 -17
  450. package/src/server/responses/request-sidecar-auth.ts +2 -2
  451. package/src/server/responses/request-transport.ts +120 -27
  452. package/src/server/responses/response-effects.ts +1 -0
  453. package/src/server/responses/run-turn-execution.ts +166 -38
  454. package/src/server/responses/sidecar-execution.ts +98 -5
  455. package/src/server/responses/terminal-guard.ts +4 -17
  456. package/src/server/responses-undeclared-tool-guard.ts +30 -9
  457. package/src/server/startup-action-control.ts +16 -2
  458. package/src/server/startup-health-cache.ts +1 -0
  459. package/src/service/auth-preflight.ts +71 -0
  460. package/src/service/cli.ts +4 -2
  461. package/src/service/definition.ts +97 -0
  462. package/src/service/desktop-startup.ts +14 -0
  463. package/src/service/guards.ts +1 -61
  464. package/src/service/health-sentinel-linux.ts +112 -0
  465. package/src/service/health-sentinel-main.ts +99 -61
  466. package/src/service/health-sentinel-probe.ts +24 -7
  467. package/src/service/health.ts +5 -89
  468. package/src/service/launchd.ts +27 -7
  469. package/src/service/logging.ts +135 -0
  470. package/src/service/repair.ts +2 -1
  471. package/src/service/state.ts +7 -8
  472. package/src/service/systemd.ts +1 -1
  473. package/src/service/windows-guarded-manager.ts +2 -1
  474. package/src/service/windows-ops.ts +1 -1
  475. package/src/service/windows-scheduler.ts +2 -7
  476. package/src/service/windows-taskxml.ts +2 -2
  477. package/src/service/windows-wscript.ts +14 -0
  478. package/src/service.ts +4 -2
  479. package/src/sidecar/auth.ts +2 -0
  480. package/src/tray/windows-tray.ps1 +43 -24
  481. package/src/types/config.ts +124 -6
  482. package/src/types/opencode-go.ts +61 -0
  483. package/src/types/opencode-zen.ts +29 -0
  484. package/src/types/provider.ts +39 -6
  485. package/src/types/request.ts +38 -2
  486. package/src/types/tools.ts +2 -0
  487. package/src/types/wire.ts +29 -37
  488. package/src/types.ts +1 -0
  489. package/src/update/async-check.ts +1 -1
  490. package/src/update/badge.ts +2 -2
  491. package/src/update/index.ts +88 -15
  492. package/src/update/job-supervised-restart.ts +107 -0
  493. package/src/update/job.ts +32 -32
  494. package/src/update/notify.ts +1 -1
  495. package/src/update/npm-invocation.mjs +12 -2
  496. package/src/update/refresh-scheduler.ts +2 -2
  497. package/src/update/supervised-recovery.d.mts +27 -0
  498. package/src/update/supervised-recovery.mjs +157 -0
  499. package/src/usage/cache-diagnostic.ts +2 -0
  500. package/src/usage/cost.ts +48 -12
  501. package/src/usage/expected-prices.ts +15 -0
  502. package/src/usage/ledger-retention.ts +2 -1
  503. package/src/usage/ledger-scanner.ts +6 -5
  504. package/src/usage/log.ts +17 -8
  505. package/src/usage/summary.ts +22 -9
  506. package/src/usage/user-cost-overlays.ts +14 -19
  507. package/src/usage/user-cost-validation.ts +38 -0
  508. package/src/vision/eligibility.ts +13 -0
  509. package/src/web-search/gemini-executor.ts +2 -2
  510. package/src/web-search/loop.ts +55 -5
  511. package/src/web-search/progress-stream.ts +4 -1
  512. package/src/web-search/run-turn-loop.ts +9 -1
  513. package/assets/sponsors/packycode.png +0 -0
  514. package/gui/dist/assets/App-CSJvmjNs.css +0 -1
  515. package/gui/dist/assets/App-CZtqiGcw.js +0 -56
  516. package/gui/dist/assets/index-BB6uK6tU.css +0 -1
  517. package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
  518. package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
@@ -1,5 +1,5 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { accessSync, constants, realpathSync, statSync } from "node:fs";
2
+ import { accessSync, constants, lstatSync, readlinkSync, realpathSync, statSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { posix, win32 } from "node:path";
5
5
  import { performance } from "node:perf_hooks";
@@ -7,6 +7,7 @@ import { isRealBunBinary } from "./bun-binary-validator.mjs";
7
7
 
8
8
  const STABLE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
9
9
  const MAX_VERSION_BYTES = 4096;
10
+ const MAX_SYMLINK_EXPANSIONS = 40;
10
11
  const IDENTITY_PROBE = "process.stdout.write(String(typeof Bun==='object'&&Bun.version))";
11
12
 
12
13
  function stableVersion(value) {
@@ -17,6 +18,62 @@ function stableVersion(value) {
17
18
  return numbers.every(Number.isSafeInteger) ? numbers : null;
18
19
  }
19
20
 
21
+ function trustedPosixCandidate(candidate, canonical, io) {
22
+ const lstat = io.lstat ?? lstatSync;
23
+ const readlink = io.readlink ?? readlinkSync;
24
+ const uid = io.geteuid?.() ?? process.geteuid?.();
25
+ if (!Number.isSafeInteger(uid) || uid < 0) return false;
26
+
27
+ const walk = path => {
28
+ let pending = path.slice(1).split("/");
29
+ let current = "/";
30
+ let expansions = 0;
31
+ while (pending.length) {
32
+ const [component, ...remaining] = pending;
33
+ pending = remaining;
34
+ if (!component || component === ".") continue;
35
+ if (component === "..") {
36
+ current = posix.dirname(current);
37
+ continue;
38
+ }
39
+ const entry = posix.join(current, component);
40
+ const info = lstat(entry);
41
+ if (!Number.isSafeInteger(info.uid) || (info.uid !== 0 && info.uid !== uid)) return null;
42
+ if (info.isSymbolicLink()) {
43
+ if (++expansions > MAX_SYMLINK_EXPANSIONS) return null;
44
+ const target = readlink(entry);
45
+ pending = target.startsWith("/")
46
+ ? [...target.slice(1).split("/"), ...pending]
47
+ : [
48
+ ...(current === "/" ? [] : current.slice(1).split("/")),
49
+ ...target.split("/"),
50
+ ...pending,
51
+ ];
52
+ current = "/";
53
+ continue;
54
+ }
55
+ if (remaining.length || pending.length) {
56
+ // No writable-ancestor exemption: a shared sticky directory passes its own
57
+ // ownership check but lets another user replace a child entry between the
58
+ // ancestry walk and the exec probe, substituting an unvalidated binary.
59
+ if (!info.isDirectory() || (info.mode & 0o022) !== 0) return null;
60
+ } else if (!info.isFile() || (info.mode & 0o022) !== 0) {
61
+ return null;
62
+ }
63
+ current = entry;
64
+ }
65
+ return current;
66
+ };
67
+
68
+ const resolved = walk(candidate);
69
+ if (!resolved || (io.realpath ?? realpathSync.native)(resolved) !== canonical) return false;
70
+ const parent = lstat(posix.dirname(canonical));
71
+ return parent.isDirectory()
72
+ && Number.isSafeInteger(parent.uid)
73
+ && (parent.uid === 0 || parent.uid === uid)
74
+ && (parent.mode & 0o022) === 0;
75
+ }
76
+
20
77
  /** Discover the first absolute PATH Bun passing version-policy and identity checks. */
21
78
  export function findPathBun({
22
79
  env = process.env, platform = process.platform, pinnedVersion,
@@ -54,14 +111,16 @@ export function findPathBun({
54
111
  if (now() >= end) break;
55
112
  if (!directory || !absolute(directory)) continue;
56
113
  try {
57
- const path = realpath(paths.join(directory, platform === "win32" ? "bun.exe" : "bun"));
114
+ const candidatePath = paths.join(directory, platform === "win32" ? "bun.exe" : "bun");
115
+ const path = realpath(candidatePath);
58
116
  if (!absolute(path)) continue;
117
+ if (platform !== "win32" && !trustedPosixCandidate(candidatePath, path, io)) continue;
59
118
  const identity = platform === "win32" ? path.toLowerCase() : path;
60
119
  if (seen.has(identity)) continue;
61
120
  seen.add(identity);
62
121
  const file = stat(path);
63
122
  if (!file.isFile()) continue;
64
- if (platform !== "win32" && ((file.mode & 0o022) || (stat(paths.dirname(path)).mode & 0o022))) continue;
123
+ if (platform !== "win32" && (file.mode & 0o022)) continue;
65
124
  access(path, constants.X_OK);
66
125
  if (!validate(path)) continue;
67
126
  const version = probe(path, ["--version"]);
@@ -36,6 +36,10 @@ export interface CodexAppServerStateResponse {
36
36
  * The desktop half of a restart. Scalar-only for the same reason as the rest of this
37
37
  * contract: pid lists and a closed-vocabulary reason, never a command line or an OS
38
38
  * error message, both of which routinely embed a path or a username.
39
+ *
40
+ * `reason: "picker_current"` means the shell was deliberately left running because it
41
+ * launched after the current catalog was written, so only the app-servers restarted and
42
+ * the operator's unsaved client state was kept (#4761).
39
43
  */
40
44
  export interface CodexDesktopRestartSummary {
41
45
  attempted: boolean;
@@ -43,8 +47,17 @@ export interface CodexDesktopRestartSummary {
43
47
  surviving: number[];
44
48
  relaunch: "started" | "skipped";
45
49
  reason?: string;
50
+ /**
51
+ * Why the desktop was or was not restarted: `stale` (the shell predates the bound
52
+ * catalog, or this request rebound it) and `unknown` (the evidence was unreadable) both
53
+ * take the full restart. Absent on a proxy older than #4761.
54
+ */
55
+ pickerState?: CodexDesktopPickerState;
46
56
  }
47
57
 
58
+ export type CodexDesktopPickerState = "current" | "stale" | "unknown" | "not_running";
59
+ const DESKTOP_PICKER_STATES: readonly string[] = ["current", "stale", "unknown", "not_running"];
60
+
48
61
  /** POST response. All four arrays are pid lists — never command lines. */
49
62
  export interface CodexRestartResponse {
50
63
  success: boolean;
@@ -130,6 +143,8 @@ export function isCodexRestartResponse(value: unknown): value is CodexRestartRes
130
143
  if (!isPidList(d.stopped) || !isPidList(d.surviving)) return false;
131
144
  if (d.relaunch !== "started" && d.relaunch !== "skipped") return false;
132
145
  if (d.reason !== undefined && typeof d.reason !== "string") return false;
146
+ if (d.pickerState !== undefined
147
+ && (typeof d.pickerState !== "string" || !DESKTOP_PICKER_STATES.includes(d.pickerState))) return false;
133
148
  // A started relaunch cannot have left anything behind: the ladder refuses to
134
149
  // relaunch beside a survivor precisely so a second shell never appears.
135
150
  if (d.relaunch === "started" && (d.surviving as number[]).length > 0) return false;
@@ -0,0 +1,30 @@
1
+ import { configuredOutboundFetch, effectiveProxyFor, noProxyMatches, type ProxyEnvMap, type ProxyCapableRequestInit } from "./proxy-env";
2
+
3
+ /** Bind both native desktop transports to one explicit route; never fall back after failure. */
4
+ export function desktopProxyFor(url: URL, env: ProxyEnvMap = process.env): string | false {
5
+ if (noProxyMatches(url, env)) return false;
6
+ const key = url.protocol === "https:" ? "HTTPS_PROXY" : "HTTP_PROXY";
7
+ let selected = effectiveProxyFor(url, env);
8
+ // Unlike an implicit Bun route, this transport explicitly binds HTTP(S) CONNECT.
9
+ // A malformed scheme-specific setting still refuses instead of falling through.
10
+ if (!selected && url.protocol === "https:" && !(env[key]?.trim() || env[key.toLowerCase()]?.trim())) {
11
+ const all = env.ALL_PROXY?.trim() || env.all_proxy?.trim();
12
+ if (all) {
13
+ let parsed: URL; try { parsed = new URL(all); } catch { throw new Error("desktop_egress_proxy_invalid"); }
14
+ if (["http:", "https:"].includes(parsed.protocol)) selected = all;
15
+ }
16
+ }
17
+ if (selected) {
18
+ const proxy = new URL(selected);
19
+ if (!["http:", "https:", "socks5:", "socks5h:"].includes(proxy.protocol) || !proxy.hostname
20
+ || proxy.search || proxy.hash || proxy.pathname !== "" && proxy.pathname !== "/") throw new Error("desktop_egress_proxy_invalid");
21
+ return selected;
22
+ }
23
+ // An explicit but unsupported route must not quietly become direct desktop traffic.
24
+ if ([env[key], env[key.toLowerCase()], env.ALL_PROXY, env.all_proxy].some(value => value?.trim())) throw new Error("desktop_egress_proxy_invalid");
25
+ return false;
26
+ }
27
+ export function desktopOutboundFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
28
+ const url = new URL(input instanceof Request ? input.url : String(input));
29
+ return configuredOutboundFetch(input, { ...init, proxy: desktopProxyFor(url) } as ProxyCapableRequestInit);
30
+ }
@@ -0,0 +1,72 @@
1
+ import { connect as tcpConnect, type Socket } from "node:net";
2
+ import { connect as tlsConnect, type TLSSocket } from "node:tls";
3
+ import { desktopProxyFor } from "./desktop-proxy-route";
4
+ import { dialTunnel, type OpaqueProxyRoute } from "./opaque-proxy-tunnel";
5
+
6
+ // The shared opaque dialer owns proxy framing; this caller owns fixed-target
7
+ // TLS and bounds the entire connection, including the target TLS handshake.
8
+ export interface DesktopTunnelOptions {
9
+ proxy?: string | false;
10
+ signal?: AbortSignal;
11
+ timeoutMs?: number;
12
+ /** Isolated test peers; production always uses verified TLS to chatgpt.com:443. */
13
+ target?: { host: string; port: number };
14
+ ca?: string;
15
+ proxyCa?: string;
16
+ }
17
+ const fail = () => new Error("desktop_upstream_connection_failed");
18
+ function waitConnected(socket: Socket, event: "connect" | "secureConnect", signal: AbortSignal): Promise<void> {
19
+ return new Promise((resolve, reject) => {
20
+ const dispose = () => { socket.off(event, done); socket.off("error", failure); socket.off("close", failure); signal.removeEventListener("abort", failure); };
21
+ const done = () => { dispose(); resolve(); };
22
+ const failure = () => { dispose(); reject(fail()); };
23
+ socket.once(event, done); socket.once("error", failure); socket.once("close", failure);
24
+ signal.addEventListener("abort", failure, { once: true }); if (signal.aborted) failure();
25
+ });
26
+ }
27
+ /** Fixed-destination TLS dial for native desktop upgraded sockets. No logs or direct fallback. */
28
+ export async function dialDesktopUpstream(options: DesktopTunnelOptions = {}): Promise<TLSSocket> {
29
+ const timeout = options.timeoutMs ?? 10000;
30
+ if (!Number.isSafeInteger(timeout) || timeout < 1 || timeout > 30000) throw fail();
31
+ const cancel = new AbortController(), timer = setTimeout(() => cancel.abort(), timeout);
32
+ const signal = options.signal ? AbortSignal.any([options.signal, cancel.signal]) : cancel.signal;
33
+ const sockets = new Set<Socket>();
34
+ const own = <T extends Socket>(socket: T): T => { sockets.add(socket); socket.on("error", () => {}); return socket; };
35
+ const destroy = () => { for (const socket of sockets) socket.destroy(); };
36
+ signal.addEventListener("abort", destroy, { once: true });
37
+ try {
38
+ if (signal.aborted) throw fail();
39
+ const proxy = options.proxy === undefined ? desktopProxyFor(new URL("https://chatgpt.com")) : options.proxy;
40
+ const target = options.target ?? { host: "chatgpt.com", port: 443 };
41
+ let raw: Socket;
42
+ if (proxy === false) {
43
+ raw = own(tcpConnect(target)); await waitConnected(raw, "connect", signal);
44
+ } else {
45
+ const url = new URL(proxy), protocol = url.protocol;
46
+ if (!["http:", "https:", "socks5:", "socks5h:"].includes(protocol) || !url.hostname || url.search || url.hash || url.pathname !== "" && url.pathname !== "/") throw fail();
47
+ const host = url.hostname.replace(/^\[|\]$/g, ""), port = Number(url.port) || (protocol === "https:" ? 443 : protocol.startsWith("socks") ? 1080 : 80);
48
+ const credentials = url.username || url.password
49
+ ? { username: decodeURIComponent(url.username), password: decodeURIComponent(url.password) }
50
+ : {};
51
+ const endpoint = { host, port, ...credentials };
52
+ const route: OpaqueProxyRoute = protocol === "https:"
53
+ ? { ...endpoint, kind: "https", proxyCa: options.proxyCa }
54
+ : { ...endpoint, kind: protocol.slice(0, -1) as "http" | "socks5" | "socks5h" };
55
+ const authority = `${target.host.includes(":") ? `[${target.host}]` : target.host}:${target.port}`;
56
+ // The route resolver remains authoritative; a failed proxy never becomes DIRECT.
57
+ raw = own(await dialTunnel(authority, route, { signal, timeoutMs: timeout, maxHeaderBytes: 65536 }));
58
+ }
59
+ if (signal.aborted || raw.destroyed) throw fail();
60
+ // This consumer historically closes its writable side after peer EOF. The
61
+ // shared opaque transport preserves half-closes for callers that need them.
62
+ raw.allowHalfOpen = false;
63
+ const secured = own(tlsConnect({ socket: raw, servername: "chatgpt.com", ca: options.ca, rejectUnauthorized: true, ALPNProtocols: ["http/1.1"] }));
64
+ // The CONNECT reader paused its stream before handing buffered bytes back. Bun's
65
+ // TLS-over-TLS path needs the outer TLS stream explicitly resumed for the new handshake.
66
+ raw.resume();
67
+ await waitConnected(secured, "secureConnect", signal);
68
+ if (signal.aborted) throw fail();
69
+ return secured;
70
+ } catch { destroy(); throw fail(); }
71
+ finally { clearTimeout(timer); signal.removeEventListener("abort", destroy); }
72
+ }
@@ -0,0 +1,62 @@
1
+ import { normalizeDeclaredToolName, type AdapterEvent } from "../types";
2
+ import { UNDECLARED_TOOL_CALL_ERROR_CODE } from "./errors";
3
+ import { releaseTranslatedEvent, type TranslatorBudget } from "./translator-budget";
4
+ import { releaseCompactionCiphertextLease } from "../responses/compaction";
5
+
6
+ /** Snapshot of current-turn declarations when an explicit empty catalog activates enforcement. */
7
+ export interface ClientToolCatalogRestriction {
8
+ declaredToolNames: ReadonlySet<string>;
9
+ bareCustomToolNames: ReadonlySet<string>;
10
+ }
11
+
12
+ export function emptyToolCatalogRefusal(
13
+ event: AdapterEvent,
14
+ catalog: ClientToolCatalogRestriction | undefined,
15
+ ): Extract<AdapterEvent, { type: "error" }> | undefined {
16
+ if (!catalog) return;
17
+ if (event.type === "tool_call_start") {
18
+ const name = normalizeDeclaredToolName(event.name, catalog.declaredToolNames, undefined, catalog.bareCustomToolNames);
19
+ if (catalog.declaredToolNames.has(name)) return;
20
+ } else if (catalog.declaredToolNames.size !== 0
21
+ || (event.type !== "tool_call_delta" && event.type !== "tool_call_end")) return;
22
+ return {
23
+ type: "error", status: 502, errorType: "upstream_error", code: UNDECLARED_TOOL_CALL_ERROR_CODE,
24
+ message: "routed provider emitted a client tool call outside the current request's explicit tool catalog",
25
+ };
26
+ }
27
+
28
+ /** Runs after hosted tool loops, before client delivery and continuation-state folding. */
29
+ export async function* guardEmptyToolCatalogStream(
30
+ events: AsyncIterable<AdapterEvent>,
31
+ catalog: ClientToolCatalogRestriction | undefined,
32
+ budget: TranslatorBudget,
33
+ ): AsyncGenerator<AdapterEvent> {
34
+ for await (const event of events) {
35
+ const refusal = emptyToolCatalogRefusal(event, catalog);
36
+ if (refusal) {
37
+ releaseTranslatedEvent(event, budget);
38
+ releaseCompactionCiphertextLease(event, budget);
39
+ }
40
+ yield refusal ?? event;
41
+ if (refusal) return;
42
+ }
43
+ }
44
+
45
+ export function guardEmptyToolCatalogEvents(
46
+ events: AdapterEvent[],
47
+ catalog: ClientToolCatalogRestriction | undefined,
48
+ budget: TranslatorBudget,
49
+ ): AdapterEvent[] {
50
+ if (!catalog) return events;
51
+ const index = events.findIndex(event => emptyToolCatalogRefusal(event, catalog) !== undefined);
52
+ if (index < 0) return events;
53
+ const refusal = emptyToolCatalogRefusal(events[index]!, catalog)!;
54
+ for (const event of events.slice(index)) {
55
+ if ((event.type === "done" || event.type === "incomplete" || event.type === "error") && event.usage) {
56
+ refusal.usage = event.usage;
57
+ }
58
+ releaseTranslatedEvent(event, budget);
59
+ releaseCompactionCiphertextLease(event, budget);
60
+ }
61
+ return [...events.slice(0, index), refusal];
62
+ }
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 {
@@ -643,10 +650,22 @@ export function adapterFailureFromMessage(message: string): { httpStatus: number
643
650
  return { httpStatus, error };
644
651
  }
645
652
 
653
+ /** A declared client refusal: retrying the identical request cannot change the verdict. */
654
+ const CLIENT_ERROR_STATUS: Record<string, 400 | 401 | 403 | 404> = {
655
+ invalid_request_error: 400,
656
+ authentication_error: 401,
657
+ permission_error: 403,
658
+ not_found_error: 404,
659
+ };
660
+
646
661
  /** Narrow structured classes carried through a synthesized bare-error terminal. */
647
662
  export function recognizedUpstreamError(error: { type?: unknown; code?: unknown }):
648
- { type: string; code: string; httpStatus: 429 | 503 } | undefined {
663
+ { type: string; code: string; httpStatus: 400 | 401 | 403 | 404 | 429 | 503 } | undefined {
649
664
  const code = typeof error.code === "string" ? error.code : undefined;
665
+ const clientClass = code ?? (typeof error.type === "string" ? error.type : undefined);
666
+ if (clientClass !== undefined && Object.hasOwn(CLIENT_ERROR_STATUS, clientClass)) {
667
+ return { type: clientClass, code: clientClass, httpStatus: CLIENT_ERROR_STATUS[clientClass] };
668
+ }
650
669
  // An explicit unknown code must not gain a recognized class from its type or copy.
651
670
  if (code !== undefined && !["rate_limit_exceeded", "rate_limit_error", "server_is_overloaded", "overloaded_error"].includes(code)) return undefined;
652
671
  if (code === "rate_limit_exceeded" || code === "rate_limit_error"
@@ -673,8 +692,13 @@ export function httpStatusFromTerminalError(error: {
673
692
  if (isCyberPolicyCode(error.code) || (error.message ? isCyberPolicyMessage(error.message) : false)) {
674
693
  return 400;
675
694
  }
695
+ const message = error.message ?? "";
676
696
  const recognized = recognizedUpstreamError(error);
677
- if (recognized) return recognized.httpStatus;
697
+ // Client-closed messages often arrive as invalid_request_error after classifyError, so a
698
+ // recognized 400 yields to the client-closed check below; every other class keeps its order.
699
+ if (recognized && !(recognized.httpStatus === 400 && message && isClientClosedMessage(message))) {
700
+ return recognized.httpStatus;
701
+ }
678
702
  if (error.type === "rate_limit_error" || error.code === "rate_limit_exceeded") return 429;
679
703
  if (error.type === "authentication_error" || error.code === "invalid_api_key") return 401;
680
704
  if (
@@ -686,7 +710,6 @@ export function httpStatusFromTerminalError(error: {
686
710
  if (error.type === "server_error" && error.code === "server_is_overloaded") return 503;
687
711
  // Client-closed messages often arrive as invalid_request_error after classifyError; check message
688
712
  // before treating every invalid_request_error as HTTP 400.
689
- const message = error.message ?? "";
690
713
  if (message && isClientClosedMessage(message)) return 499;
691
714
  if (error.type === "invalid_request_error") return 400;
692
715
  if (error.type === "proxy_error") return 500;
@@ -2,6 +2,10 @@
2
2
  * Application Default Credentials (ADC) resolution for Vertex AI.
3
3
  *
4
4
  * Direct WebCrypto + REST implementation (no `google-auth-library`). Sources, in priority order:
5
+ * 0. A `gcp-sa:<account>` reference (the provider's active key resolving to one) → the Service
6
+ * Account / authorized_user JSON stored in the OS credential store under that account
7
+ * (`keychain:<account>` secret, written by the key-store save path when the user pastes a
8
+ * credential JSON directly into the API-key slot). See resolveGcpCredentialMarker.
5
9
  * 1. `GOOGLE_APPLICATION_CREDENTIALS` env → file with `type: "service_account"` (RS256 JWT
6
10
  * exchange) or `type: "authorized_user"` (refresh-token exchange).
7
11
  * 2. gcloud user ADC (`CLOUDSDK_CONFIG`, else `%APPDATA%\gcloud` on Windows, else
@@ -18,6 +22,8 @@ import { Buffer } from "node:buffer";
18
22
  import * as os from "node:os";
19
23
  import * as path from "node:path";
20
24
  import { readFileSync, existsSync, statSync } from "node:fs";
25
+ import { GCP_CREDENTIAL_MARKER_PREFIX, providerKeychainEntry } from "../providers/api-key-resolve";
26
+ export { isGcpCredentialMaterial } from "./gcp-credential-input";
21
27
  import {
22
28
  captureConfigGeneration,
23
29
  sweepExpiredOnWrite,
@@ -36,6 +42,50 @@ const TOKEN_TIMEOUT_MS = 15_000;
36
42
  const TOKEN_ATTEMPTS = 3;
37
43
  const TOKEN_RETRY_BASE_MS = 300;
38
44
 
45
+ /**
46
+ * Reference prefix stored in `provider.apiKey` (and `apiKeyPool` entries) when the user pastes a
47
+ * GCP credential JSON directly into an API-key slot. The secret JSON lives in the OS credential
48
+ * store under the same service the `keychain:` references use; only the marker is persisted to
49
+ * `config.json`. The leaf reference prefix also identifies already-stored material for migration.
50
+ */
51
+ export { GCP_CREDENTIAL_MARKER_PREFIX } from "../providers/api-key-resolve";
52
+
53
+ /** Extract a marker account; malformed explicit markers never opt into host ADC. */
54
+ export function gcpCredentialMarkerAccount(value: string | undefined): string | undefined {
55
+ if (typeof value !== "string" || !value.startsWith(GCP_CREDENTIAL_MARKER_PREFIX)) return undefined;
56
+ const account = value.slice(GCP_CREDENTIAL_MARKER_PREFIX.length);
57
+ if (!account.trim()) throw new Error("GCP credential marker requires a non-empty account");
58
+ return account;
59
+ }
60
+
61
+ /**
62
+ * Whether a pasted API-key value is in fact a GCP credential JSON. Used by the save path to
63
+ * divert JSON material out of the literal-key flow before it reaches `config.json`. Deliberately
64
+ * strict: only a `type` field with a known ADC credential type counts, so a user's literal API
65
+ * key that happens to start with `{` is never eaten.
66
+ */
67
+ export function parseGcpCredentialJson(value: string): AdcFileCredentials | undefined {
68
+ const trimmed = (value ?? "").trim();
69
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return undefined;
70
+ try {
71
+ const parsed = JSON.parse(trimmed) as Record<string, unknown>;
72
+ if (parsed?.type === "service_account") {
73
+ if (typeof parsed.client_email !== "string" || !parsed.client_email.trim()) return undefined;
74
+ if (typeof parsed.private_key !== "string" || !parsed.private_key.trim()) return undefined;
75
+ return parsed as unknown as AdcFileCredentials;
76
+ }
77
+ if (parsed?.type === "authorized_user") {
78
+ if (typeof parsed.client_id !== "string" || !parsed.client_id.trim()) return undefined;
79
+ if (typeof parsed.client_secret !== "string" || !parsed.client_secret.trim()) return undefined;
80
+ if (typeof parsed.refresh_token !== "string" || !parsed.refresh_token.trim()) return undefined;
81
+ return parsed as unknown as AdcFileCredentials;
82
+ }
83
+ return undefined;
84
+ } catch {
85
+ return undefined;
86
+ }
87
+ }
88
+
39
89
  interface CachedToken {
40
90
  token: string;
41
91
  expiresAtMs: number;
@@ -81,7 +131,10 @@ export function sweepExpiredGcpAdcTokens(now = Date.now()): number {
81
131
 
82
132
  export function reconcileGcpAdcTokens(context: GenerationContext): number {
83
133
  if (context.generation <= lastReconciledGeneration) return 0;
84
- const currentSource = currentAdcSourceKey();
134
+ // The periodic sweep registration has no request context, so no marker is active here: markers
135
+ // travel per-request via getVertexAccessToken(options.markerAccount), and a marker-token cache
136
+ // entry whose account no longer resolves is simply pruned by this sweep when it expires.
137
+ const currentSource = currentAdcSourceKey(undefined);
85
138
  let removed = 0;
86
139
  for (const source of tokenCache.keys()) {
87
140
  if (source === currentSource) continue;
@@ -131,7 +184,29 @@ function readJsonFile<T>(filePath: string): T | undefined {
131
184
  return JSON.parse(readFileSync(filePath, "utf8")) as T;
132
185
  }
133
186
 
134
- function loadAdcCredentials(): { source: string; creds: AdcFileCredentials } | undefined {
187
+ /**
188
+ * Read the credential JSON for a `gcp-sa:<account>` marker from the OS credential store. Uses the
189
+ * same service name and the same injectable entry factory as the `keychain:` API-key references
190
+ * (api-key-resolve.providerKeychainEntry), so one probe surface and one test seam cover both.
191
+ * Mirrors `readKeychain`'s fail-closed policy: an unreadable entry yields no credential (the
192
+ * caller surfaces the failure instead of silently falling through to the next ADC source).
193
+ */
194
+ function readMarkerCredential(account: string): AdcFileCredentials | undefined {
195
+ try {
196
+ const raw = providerKeychainEntry(account).getPassword();
197
+ if (typeof raw !== "string" || !raw.trim()) return undefined;
198
+ return parseGcpCredentialJson(raw);
199
+ } catch {
200
+ return undefined;
201
+ }
202
+ }
203
+
204
+ function loadAdcCredentials(markerAccount: string | undefined): { source: string; creds: AdcFileCredentials } | undefined {
205
+ if (markerAccount) {
206
+ const creds = readMarkerCredential(markerAccount);
207
+ if (!creds) throw new Error("GCP credential marker is set but its credential could not be read from the OS credential store");
208
+ return { source: `marker:${markerAccount}`, creds };
209
+ }
135
210
  const gacPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;
136
211
  if (gacPath) {
137
212
  const creds = readJsonFile<AdcFileCredentials>(gacPath);
@@ -148,9 +223,10 @@ function loadAdcCredentials(): { source: string; creds: AdcFileCredentials } | u
148
223
  * The cache key for the source the NEXT resolve would use, computed cheaply (no network). Lets the
149
224
  * cache return a token only when it still matches the active credential source, so an in-process
150
225
  * change to GOOGLE_APPLICATION_CREDENTIALS (or the user ADC file) does not keep serving a stale
151
- * token from a different source. Falls back to "metadata" when no file/env ADC is present.
226
+ * token from a different source. Falls back to "metadata" when no marker/file/env ADC is present.
152
227
  */
153
- function currentAdcSourceKey(): string {
228
+ function currentAdcSourceKey(markerAccount: string | undefined): string {
229
+ if (markerAccount) return `marker:${markerAccount}`;
154
230
  const gacPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;
155
231
  if (gacPath) return fileSourceTag("gac", gacPath);
156
232
  const userPath = userAdcPath();
@@ -279,8 +355,8 @@ async function fetchMetadataToken(signal: AbortSignal | undefined, fetchImpl: Fe
279
355
  }
280
356
  }
281
357
 
282
- async function resolveAccessTokenUncached(signal: AbortSignal | undefined, fetchImpl: FetchImpl): Promise<{ source: string; token: TokenResponse }> {
283
- const adc = loadAdcCredentials();
358
+ async function resolveAccessTokenUncached(markerAccount: string | undefined, signal: AbortSignal | undefined, fetchImpl: FetchImpl): Promise<{ source: string; token: TokenResponse }> {
359
+ const adc = loadAdcCredentials(markerAccount);
284
360
  if (adc) {
285
361
  const token = adc.creds.type === "service_account"
286
362
  ? await exchangeJwtForToken(adc.creds, signal, fetchImpl)
@@ -295,14 +371,25 @@ async function resolveAccessTokenUncached(signal: AbortSignal | undefined, fetch
295
371
  }
296
372
 
297
373
  /** Returns a Bearer access token for the `Authorization` header on Vertex AI calls (cached + refreshed). */
298
- export async function getVertexAccessToken(options?: { signal?: AbortSignal; fetch?: FetchImpl }): Promise<string> {
374
+ export async function getVertexAccessToken(options?: { signal?: AbortSignal; fetch?: FetchImpl; markerAccount?: string }): Promise<string> {
375
+ // The marker travels as an argument, never as process-global state: a module-level "active
376
+ // marker" written by the router would race across concurrent requests for different providers
377
+ // and could resolve one provider's Vertex call with another provider's service account. The
378
+ // adapter passes `gcpCredentialMarkerAccount(provider.apiKey)` per request instead.
379
+ const markerAccount = options?.markerAccount;
299
380
  const fetchImpl = options?.fetch ?? globalThis.fetch.bind(globalThis);
300
381
  const skew = getRefreshSkewMs();
301
382
  const now = Date.now();
302
383
 
303
384
  // Only serve a cached token that matches the source the next resolve would actually use; prune
304
385
  // expired or now-stale (different-source) entries so a credential-source change is honored.
305
- const expectedSource = currentAdcSourceKey();
386
+ const expectedSource = currentAdcSourceKey(markerAccount);
387
+ // A cached bearer token is usable only while the explicitly selected store entry remains
388
+ // readable. Removal or a locked store must fail closed before the cache can return it.
389
+ if (markerAccount && !readMarkerCredential(markerAccount)) {
390
+ tokenCache.delete(expectedSource);
391
+ throw new Error("GCP credential marker is set but its credential could not be read from the OS credential store");
392
+ }
306
393
  const writerGeneration = captureConfigGeneration();
307
394
  for (const [source, cached] of tokenCache) {
308
395
  if (source === expectedSource && cached.expiresAtMs - skew > now) return cached.token;
@@ -317,7 +404,7 @@ export async function getVertexAccessToken(options?: { signal?: AbortSignal; fet
317
404
 
318
405
  const promise = (async () => {
319
406
  try {
320
- const { source, token } = await resolveAccessTokenUncached(options?.signal, fetchImpl);
407
+ const { source, token } = await resolveAccessTokenUncached(markerAccount, options?.signal, fetchImpl);
321
408
  const expiresAtMs = Date.now() + Math.max(0, token.expires_in * 1000);
322
409
  if (writerGeneration >= lastReconciledGeneration || liveSources.has(source)) {
323
410
  tokenCache.set(source, { token: token.access_token, expiresAtMs });
@@ -0,0 +1,12 @@
1
+ /** Structured JSON is never literal key material; malformed GCP text is also refused. */
2
+ export function isGcpCredentialMaterial(value: string): boolean {
3
+ const credentialField = /\b(?:service_account|authorized_user|external_account|private_key|refresh_token|client_secret)\b|-----BEGIN [A-Z ]*PRIVATE KEY-----/;
4
+ if (/[\[\{"']/.test(value) && credentialField.test(value)) return true;
5
+ // Reject structured carriers outright rather than recursively guessing their credential shape.
6
+ // This includes quoted/array/object wrappers with arbitrarily escaped or nested JSON inside.
7
+ try {
8
+ const parsed: unknown = JSON.parse(value);
9
+ return typeof parsed === "string" || (parsed !== null && typeof parsed === "object");
10
+ }
11
+ catch { return false; }
12
+ }
@@ -72,6 +72,11 @@ export async function buildLabProviderAuthHeaders(
72
72
  && configuredAnthropicInstance(config, routeContext.providerId) !== routeContext.providerId) {
73
73
  throw new TransportError("auth_blocked", "anthropic instance unavailable");
74
74
  }
75
+ // The DSH account token is sent only by its own adapter to its pinned endpoint; the generic
76
+ // probe transport would use another endpoint and auth scheme.
77
+ if (routeContext.providerId === "dsh-account") {
78
+ throw new TransportError("auth_blocked", "dsh-account probes are not supported");
79
+ }
75
80
  try {
76
81
  const snapshot = await getValidAccessTokenSnapshot(routeContext.providerId);
77
82
  headers.Authorization = `Bearer ${snapshot.accessToken}`;
@@ -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
+ }