@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
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Who refreshes the native main ChatGPT credential (#2999).
3
+ *
4
+ * Codex owns `CODEX_HOME/auth.json`. It rewrites the file in place and serializes its own
5
+ * refreshes only inside one process, so any second writer can race it, and a lost race
6
+ * retires the grant. In "codex" mode OpenCodex never writes the file: it asks the Codex
7
+ * binary to refresh through `app-server` `getAuthStatus` and uses the bearer that call
8
+ * returns. In "opencodex" mode the guarded writer in `main-account.ts` refreshes as before.
9
+ *
10
+ * The default is "codex" only when a version-checked binary is available. Without one, or
11
+ * when the operator sets `nativeMainRefresh: "opencodex"`, the guarded writer stays.
12
+ */
13
+ import { codexExecInvocation } from "./exec-invocation";
14
+ import { resolveCodexHomeDir } from "./home";
15
+ import { compareCodexVersions, displayCodexRuntimePath, resolveCodexRuntimeAsync } from "./runtime";
16
+ import type { ResolveCodexRuntimeResult } from "./runtime";
17
+ import { isTestHomeGuardArmed } from "../lib/test-home-guard";
18
+ import type { OcxConfig } from "../types/config";
19
+
20
+ export type NativeMainRefreshMode = "codex" | "opencodex";
21
+
22
+ /**
23
+ * Supported floor for app-server `getAuthStatus` with `includeToken`/`refreshToken` that
24
+ * withholds the token after a permanent refresh failure, the only reauth signal on the wire.
25
+ * Releases from about 0.118 already withhold it; 0.130.0 is the first with the handler in
26
+ * `request_processors/account_processor.rs` and is chosen conservatively.
27
+ */
28
+ export const CODEX_AUTH_STATUS_MIN_VERSION = "0.130.0";
29
+
30
+ export type NativeMainRefreshReason =
31
+ | "codex-compatible"
32
+ | "configured-opencodex"
33
+ | "codex-missing"
34
+ | "codex-too-old"
35
+ | "test-guard";
36
+
37
+ export interface NativeMainRefreshResolution {
38
+ readonly mode: NativeMainRefreshMode;
39
+ readonly configured: NativeMainRefreshMode | "auto";
40
+ readonly reason: NativeMainRefreshReason;
41
+ readonly command?: string;
42
+ readonly version?: string | null;
43
+ }
44
+
45
+ export interface CodexAuthStatus {
46
+ readonly authMethod: string | null;
47
+ readonly authToken: string | null;
48
+ readonly requiresOpenaiAuth: boolean | null;
49
+ }
50
+
51
+ export class CodexAuthStatusError extends Error {
52
+ constructor(message: string, options?: ErrorOptions) {
53
+ super(message, options);
54
+ this.name = "CodexAuthStatusError";
55
+ }
56
+ }
57
+
58
+ let resolutionForTests: NativeMainRefreshResolution | null = null;
59
+
60
+ /** Test seam: pin the resolution so no test spawns the developer's real Codex binary. */
61
+ export function setNativeMainRefreshResolutionForTests(value: NativeMainRefreshResolution | null): void {
62
+ resolutionForTests = value;
63
+ }
64
+
65
+ function configuredMode(config: Pick<OcxConfig, "nativeMainRefresh"> | null | undefined): NativeMainRefreshMode | "auto" {
66
+ const value = config?.nativeMainRefresh;
67
+ return value === "codex" || value === "opencodex" ? value : "auto";
68
+ }
69
+
70
+ export async function resolveNativeMainRefreshMode(
71
+ config: Pick<OcxConfig, "nativeMainRefresh"> | null | undefined,
72
+ deps: { resolveRuntime?: () => Promise<ResolveCodexRuntimeResult> } = {},
73
+ ): Promise<NativeMainRefreshResolution> {
74
+ if (resolutionForTests) return resolutionForTests;
75
+ const configured = configuredMode(config);
76
+ if (configured === "opencodex") return { mode: "opencodex", configured, reason: "configured-opencodex" };
77
+ // Tests run against throwaway CODEX_HOMEs; delegating would spawn the real binary.
78
+ if (!deps.resolveRuntime && isTestHomeGuardArmed()) return { mode: "opencodex", configured, reason: "test-guard" };
79
+ const resolved = await (deps.resolveRuntime ?? (() => resolveCodexRuntimeAsync({ discoverAlternatives: false })))();
80
+ const { command, version, source } = resolved.runtime;
81
+ if (source === "fallback" && version === null) {
82
+ return { mode: "opencodex", configured, reason: "codex-missing" };
83
+ }
84
+ if (version === null || compareCodexVersions(version, CODEX_AUTH_STATUS_MIN_VERSION) < 0) {
85
+ return { mode: "opencodex", configured, reason: "codex-too-old", command, version };
86
+ }
87
+ return { mode: "codex", configured, reason: "codex-compatible", command, version };
88
+ }
89
+
90
+ /**
91
+ * One `getAuthStatus` round trip against a short-lived `codex app-server` on stdio.
92
+ *
93
+ * `model_provider="openai"` is forced because an authless Desktop injection sets
94
+ * `requires_openai_auth = false` on the OpenCodex provider, and then the server reports no
95
+ * credential at all. The process inherits CODEX_HOME so it reads the same login (file or
96
+ * keyring) Codex itself uses.
97
+ */
98
+ export async function requestCodexAuthStatus(
99
+ command: string,
100
+ options: { refresh: boolean; signal: AbortSignal; codexHome?: string },
101
+ ): Promise<CodexAuthStatus> {
102
+ if (options.signal.aborted) throw options.signal.reason;
103
+ const invocation = codexExecInvocation(command, ["-c", 'model_provider="openai"', "app-server"]);
104
+ let proc: ReturnType<typeof Bun.spawn<"pipe", "pipe", "ignore">>;
105
+ try {
106
+ proc = Bun.spawn([invocation.file, ...invocation.args], {
107
+ env: { ...process.env, CODEX_HOME: options.codexHome ?? resolveCodexHomeDir() },
108
+ stdin: "pipe",
109
+ stdout: "pipe",
110
+ stderr: "ignore",
111
+ windowsVerbatimArguments: invocation.options.windowsVerbatimArguments,
112
+ windowsHide: true,
113
+ });
114
+ } catch (cause) {
115
+ throw new CodexAuthStatusError("could not start codex app-server", { cause });
116
+ }
117
+ // Abort settles the call by itself. Waiting for stdout to close would hang on a child that
118
+ // ignores SIGTERM or a launcher descendant that keeps the pipe open.
119
+ let onAbort = () => {};
120
+ const aborted = new Promise<never>((_, reject) => {
121
+ onAbort = () => reject(options.signal.reason);
122
+ options.signal.addEventListener("abort", onAbort, { once: true });
123
+ });
124
+ aborted.catch(() => { /* observed through the race below */ });
125
+ try {
126
+ const send = (message: unknown) => proc.stdin.write(JSON.stringify(message) + "\n");
127
+ send({ id: 1, method: "initialize", params: { clientInfo: { name: "opencodex", title: null, version: "0" } } });
128
+ send({ method: "initialized" });
129
+ send({ id: 2, method: "getAuthStatus", params: { includeToken: true, refreshToken: options.refresh } });
130
+ await Promise.race([proc.stdin.flush(), aborted]);
131
+ const response = await Promise.race([readResponse(proc.stdout, 2), aborted]);
132
+ if (options.signal.aborted) throw options.signal.reason;
133
+ return parseAuthStatus(response);
134
+ } catch (cause) {
135
+ if (options.signal.aborted) throw options.signal.reason;
136
+ if (cause instanceof CodexAuthStatusError) throw cause;
137
+ throw new CodexAuthStatusError("codex app-server getAuthStatus failed", { cause });
138
+ } finally {
139
+ options.signal.removeEventListener("abort", onAbort);
140
+ stopAppServer(proc);
141
+ }
142
+ }
143
+
144
+ const APP_SERVER_KILL_GRACE_MS = 2_000;
145
+
146
+ /** SIGTERM now, SIGKILL if the child is still alive after a grace period; never awaited. */
147
+ function stopAppServer(proc: { stdin: { end(): unknown }; kill(signal?: number | NodeJS.Signals): void; exited: Promise<number> }): void {
148
+ try { proc.stdin.end(); } catch { /* already closed */ }
149
+ let exited = false;
150
+ void proc.exited.then(() => { exited = true; }, () => { exited = true; });
151
+ try { proc.kill("SIGTERM"); } catch { /* already gone */ }
152
+ const timer = setTimeout(() => {
153
+ if (!exited) try { proc.kill("SIGKILL"); } catch { /* already gone */ }
154
+ }, APP_SERVER_KILL_GRACE_MS);
155
+ (timer as { unref?: () => void }).unref?.();
156
+ }
157
+
158
+ const MAX_APP_SERVER_LINE_BYTES = 1_048_576;
159
+
160
+ async function readResponse(stream: ReadableStream<Uint8Array>, id: number): Promise<Record<string, unknown>> {
161
+ const decoder = new TextDecoder();
162
+ let buffered = "";
163
+ for await (const chunk of stream) {
164
+ buffered += decoder.decode(chunk, { stream: true });
165
+ let newline: number;
166
+ while ((newline = buffered.indexOf("\n")) >= 0) {
167
+ const line = buffered.slice(0, newline).trim();
168
+ buffered = buffered.slice(newline + 1);
169
+ if (!line) continue;
170
+ let message: unknown;
171
+ try { message = JSON.parse(line); } catch { continue; }
172
+ if (!message || typeof message !== "object") continue;
173
+ const record = message as Record<string, unknown>;
174
+ if (record.id !== id) continue;
175
+ if (record.error !== undefined) throw new CodexAuthStatusError("codex app-server rejected getAuthStatus");
176
+ if (!record.result || typeof record.result !== "object") {
177
+ throw new CodexAuthStatusError("codex app-server returned no getAuthStatus result");
178
+ }
179
+ return record.result as Record<string, unknown>;
180
+ }
181
+ if (buffered.length > MAX_APP_SERVER_LINE_BYTES) throw new CodexAuthStatusError("codex app-server line too long");
182
+ }
183
+ throw new CodexAuthStatusError("codex app-server exited before answering getAuthStatus");
184
+ }
185
+
186
+ function parseAuthStatus(result: Record<string, unknown>): CodexAuthStatus {
187
+ const authMethod = typeof result.authMethod === "string" ? result.authMethod : null;
188
+ const authToken = typeof result.authToken === "string" && result.authToken ? result.authToken : null;
189
+ const requiresOpenaiAuth = typeof result.requiresOpenaiAuth === "boolean" ? result.requiresOpenaiAuth : null;
190
+ return { authMethod, authToken, requiresOpenaiAuth };
191
+ }
192
+
193
+ /** `ocx doctor` lines for the active refresh owner. Never includes a credential. */
194
+ export function formatNativeMainRefreshDoctorLines(resolution: NativeMainRefreshResolution): string[] {
195
+ const configured = resolution.configured === "auto" ? "auto" : `nativeMainRefresh: "${resolution.configured}"`;
196
+ const binary = resolution.command
197
+ ? `${displayCodexRuntimePath(resolution.command)} (${resolution.version ?? "unknown"})`
198
+ : "none";
199
+ switch (resolution.reason) {
200
+ case "codex-compatible":
201
+ return [` ok codex: Codex refreshes the main login through app-server (${binary}; ${configured})`];
202
+ case "configured-opencodex":
203
+ return [` -- opencodex: OpenCodex refreshes auth.json with its guarded writer (${configured})`];
204
+ case "test-guard":
205
+ return [" -- opencodex: test guard is armed; Codex delegation is disabled"];
206
+ case "codex-missing":
207
+ return [
208
+ ` !! opencodex: no Codex binary found, so OpenCodex refreshes auth.json itself (${configured})`,
209
+ ` Install Codex ${CODEX_AUTH_STATUS_MIN_VERSION} or newer to let Codex own its login.`,
210
+ ];
211
+ case "codex-too-old":
212
+ return [
213
+ ` !! opencodex: Codex ${binary} is older than ${CODEX_AUTH_STATUS_MIN_VERSION}, so OpenCodex refreshes auth.json itself (${configured})`,
214
+ " Update Codex to let Codex own its login.",
215
+ ];
216
+ }
217
+ }
@@ -3,7 +3,7 @@ import { loadConfig } from "../config";
3
3
  import { initializeMainAccountPolicyBinding } from "./account-lifecycle";
4
4
  import { isMainAccountHardLockEnabled } from "./main-account-hard-lock";
5
5
  import { clearAccountNeedsReauth } from "./account-runtime-state";
6
- import { MAIN_CODEX_ACCOUNT_ID } from "./main-account";
6
+ import { MAIN_CODEX_ACCOUNT_ID } from "./account-id";
7
7
  import {
8
8
  probeNativeProfileRecoveryState,
9
9
  resolveNativeProfileContext,
@@ -26,6 +26,7 @@ import {
26
26
  isCompactionItemType,
27
27
  SUMMARY_PREFIX,
28
28
  } from "../responses/compaction";
29
+ import { decodeRetainedCompaction } from "../responses/retained-compaction";
29
30
  import { resolveCodexHomeDir } from "./home";
30
31
  import { resolveCodexStateDbPath } from "./paths";
31
32
 
@@ -96,21 +97,22 @@ function isRecord(value: unknown): value is Record<string, unknown> {
96
97
  return value !== null && typeof value === "object" && !Array.isArray(value);
97
98
  }
98
99
 
99
- function lowerCompactionItem(item: unknown): { item: unknown; changed: boolean } {
100
+ function lowerCompactionItem(item: unknown): { items: unknown[]; changed: boolean } {
100
101
  if (!isRecord(item) || !isCompactionItemType(item.type)) {
101
- return { item, changed: false };
102
+ return { items: [item], changed: false };
102
103
  }
103
104
  if (typeof item.encrypted_content !== "string") {
104
- return { item, changed: false };
105
+ return { items: [item], changed: false };
105
106
  }
106
- const summary = decodeCompactionSummary(item.encrypted_content);
107
- if (summary === null) return { item, changed: false };
107
+ const retained = decodeRetainedCompaction(item.encrypted_content);
108
+ const summary = retained?.summary ?? decodeCompactionSummary(item.encrypted_content);
109
+ if (summary === null) return { items: [item], changed: false };
108
110
  return {
109
- item: {
111
+ items: [{
110
112
  type: "message",
111
113
  role: "user",
112
114
  content: [{ type: "input_text", text: `${SUMMARY_PREFIX}\n${summary}` }],
113
- },
115
+ }, ...(retained?.reasoning ?? []).map(text => ({ type: "message", role: "user", content: [{ type: "input_text", text }] }))],
114
116
  changed: true,
115
117
  };
116
118
  }
@@ -129,10 +131,10 @@ function rewriteJsonlLine(line: string): { line: string; replaced: number } {
129
131
  if (!Array.isArray(history)) return { line, replaced: 0 };
130
132
 
131
133
  let replaced = 0;
132
- const replacementHistory = history.map(item => {
134
+ const replacementHistory = history.flatMap(item => {
133
135
  const lowered = lowerCompactionItem(item);
134
136
  if (lowered.changed) replaced += 1;
135
- return lowered.item;
137
+ return lowered.items;
136
138
  });
137
139
  if (replaced === 0) return { line, replaced: 0 };
138
140
 
@@ -146,7 +148,7 @@ function rewriteJsonlLine(line: string): { line: string; replaced: number } {
146
148
  }
147
149
 
148
150
  /**
149
- * Convert OpenCodeX-owned `ocx1:` compaction items into ordinary replayable user messages.
151
+ * Convert OpenCodeX-owned `ocx1:`/`ocx2:` compaction items into ordinary replayable user messages.
150
152
  *
151
153
  * Only the authoritative `compacted.payload.replacement_history` snapshot is changed. Earlier
152
154
  * response-item events are historical output and are deliberately preserved byte-for-byte.
@@ -1,4 +1,4 @@
1
- import { readFileSync, realpathSync, statSync } from "node:fs";
1
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
2
2
  import { isAbsolute, join, resolve } from "node:path";
3
3
  import { expandUserPath } from "../config";
4
4
  import { defaultCodexHome } from "./home";
@@ -144,3 +144,88 @@ export function readRootTomlString(content: string, key: string): string | null
144
144
  export function resolveCodexConfigPath(path: string): string {
145
145
  return isAbsolute(path) ? path : join(CODEX_HOME, path);
146
146
  }
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // Active Codex home resolution.
150
+ //
151
+ // These resolve `CODEX_HOME` at call time rather than at import time, which is
152
+ // what lets a test or a sibling-home probe point fixture state somewhere else
153
+ // after this module has already loaded. They live here, next to the constants
154
+ // they fall back to, so a caller that only needs a path never has to reach the
155
+ // catalog parser.
156
+ // ---------------------------------------------------------------------------
157
+
158
+ export function samePath(a: string, b: string): boolean {
159
+ const left = resolve(a);
160
+ const right = resolve(b);
161
+ return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
162
+ }
163
+
164
+ export function activeCodexHome(): string | null {
165
+ const raw = process.env.CODEX_HOME?.trim();
166
+ if (!raw) return null;
167
+ const path = resolve(expandUserPath(raw));
168
+ try {
169
+ return realpathSync.native(path);
170
+ } catch {
171
+ return path;
172
+ }
173
+ }
174
+
175
+ export function activeCodexConfigPath(): string {
176
+ const home = activeCodexHome();
177
+ return home ? join(home, "config.toml") : CODEX_CONFIG_PATH;
178
+ }
179
+
180
+ export function activeDefaultCatalogPath(): string {
181
+ const home = activeCodexHome();
182
+ return home ? join(home, "opencodex-catalog.json") : DEFAULT_CATALOG_PATH;
183
+ }
184
+
185
+ export function activeCodexModelsCachePath(): string {
186
+ const home = activeCodexHome();
187
+ return home ? join(home, "models_cache.json") : CODEX_MODELS_CACHE_PATH;
188
+ }
189
+
190
+ export function resolveActiveCodexConfigPath(path: string): string {
191
+ const home = activeCodexHome();
192
+ return home ? resolve(home, path) : resolveCodexConfigPath(path);
193
+ }
194
+
195
+ export function isDefaultCatalogPath(path: string): boolean {
196
+ return samePath(path, activeDefaultCatalogPath());
197
+ }
198
+
199
+ export function readCodexCatalogPath(): string {
200
+ const home = activeCodexHome();
201
+ if (home) return readCodexCatalogPathForHome(home);
202
+ try {
203
+ const configPath = activeCodexConfigPath();
204
+ if (existsSync(configPath)) {
205
+ const toml = readFileSync(configPath, "utf-8");
206
+ const path = readRootTomlString(toml, "model_catalog_json");
207
+ if (path) return resolveActiveCodexConfigPath(path);
208
+ }
209
+ } catch { /* ignore */ }
210
+ return activeDefaultCatalogPath();
211
+ }
212
+
213
+ /**
214
+ * Resolve the configured catalog without consulting ambient CODEX_HOME again.
215
+ *
216
+ * `configText` is for a caller that has already read that same `config.toml` under
217
+ * its own constraints - the prompt-text probe reads it bounded, on the request
218
+ * thread - so resolving the catalog does not cost a second, unbounded read of the
219
+ * file the caller is holding. Omitting it keeps the original behaviour.
220
+ */
221
+ export function readCodexCatalogPathForHome(codexHome: string, configText?: string): string {
222
+ try {
223
+ const configPath = join(codexHome, "config.toml");
224
+ if (configText !== undefined || existsSync(configPath)) {
225
+ const toml = configText ?? readFileSync(configPath, "utf-8");
226
+ const path = readRootTomlString(toml, "model_catalog_json");
227
+ if (path) return resolve(codexHome, path);
228
+ }
229
+ } catch { /* ignore */ }
230
+ return join(codexHome, "opencodex-catalog.json");
231
+ }
@@ -0,0 +1,174 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { z } from "zod";
5
+ import { loadConfig } from "../config";
6
+ import { shouldSyncCodexOnStart } from "./desired-state";
7
+ import { atomicWriteFile } from "../config/atomic-write";
8
+ import { withConfigMutationLockSync } from "../config/mutation-lock";
9
+ import { getConfigDir } from "../config/paths";
10
+ import { assertCodexHomeOwner } from "./codex-home-owner";
11
+ import { ConfigWriteLockRefusal, publishConfigWriteTarget, watchConfigWriteTargets, withConfigWriteLockHeld, type LockHandle } from "./config-write-lock";
12
+ import { providerTableStart } from "./injected-marker";
13
+ import { CODEX_CONFIG_PATH, getCodexHome, tomlString } from "./paths";
14
+ import { rootSourceLines, sourceAssignment, sourceText } from "./toml-source-lines";
15
+ import { JOURNAL_PATH, removeJournal } from "./journal";
16
+ import { externalCodexModelProvider } from "./inject/config-toml";
17
+ import { readBoundedCodexConfig } from "./inject/bounded-config-reader";
18
+ import { isLoopbackHostname } from "./loopback-target";
19
+ import { validateCodexRoutingTarget, type CodexRoutingTarget } from "./inject/routing-target";
20
+
21
+ const approval = z.object({ original: z.string(), originalUrl: z.string().url(), managedUrls: z.array(z.string().url()).max(2) }).strict();
22
+ const recordSchema = z.object({ version: z.literal(1), codexHome: z.string(), providers: z.record(z.string(), approval) }).strict();
23
+ type RoutingRecord = z.infer<typeof recordSchema>;
24
+ type Approval = z.infer<typeof approval>;
25
+ let beforePublishForTests: (() => void) | undefined;
26
+ export function setBeforeProviderRoutingPublishForTests(hook: typeof beforePublishForTests): void { beforePublishForTests = hook; }
27
+
28
+ function recordPath(): string {
29
+ const homeId = createHash("sha256").update(getCodexHome()).digest("hex");
30
+ return join(getConfigDir(), `codex-provider-routing-${homeId}.json`);
31
+ }
32
+
33
+ function readRecord(): RoutingRecord {
34
+ const content = readBoundedCodexConfig(recordPath());
35
+ if (content === null) return { version: 1, codexHome: getCodexHome(), providers: {} };
36
+ const record = recordSchema.parse(JSON.parse(content));
37
+ if (record.codexHome !== getCodexHome()) throw new Error("Codex provider routing consent belongs to another home.");
38
+ for (const saved of Object.values(record.providers)) {
39
+ const parsed = Bun.TOML.parse(saved.original) as Record<string, unknown>;
40
+ if (Object.keys(parsed).length !== 1 || parsed.base_url !== saved.originalUrl
41
+ || saved.managedUrls.some(url => !isLoopbackHostname(new URL(url).hostname))) {
42
+ throw new Error("Codex provider routing recovery evidence is invalid; existing files were preserved.");
43
+ }
44
+ }
45
+ return record;
46
+ }
47
+
48
+ /** Only regular Responses provider tables with no separate credential transport are eligible. */
49
+ function providerUrl(content: string, id: string, checkCredentials = true) {
50
+ if (!/^[A-Za-z0-9_-]{1,128}$/.test(id) || ["__proto__", "constructor", "prototype", "openai", "opencodex", "ollama", "lmstudio"].includes(id)) {
51
+ throw new Error("Select a custom provider ID using letters, digits, hyphens or underscores.");
52
+ }
53
+ const parsed = Bun.TOML.parse(content) as Record<string, unknown>;
54
+ const table = (parsed.model_providers as Record<string, Record<string, unknown>> | undefined)?.[id];
55
+ if (!table || checkCredentials && ((table.wire_api !== undefined && table.wire_api !== "responses")
56
+ || table.requires_openai_auth !== true
57
+ || ["env_key", "experimental_bearer_token", "auth", "http_headers", "env_http_headers"].some(key => Object.hasOwn(table, key)))) {
58
+ throw new Error("Routing consent requires a Responses provider using OpenAI authentication without custom credentials or headers.");
59
+ }
60
+ const { bom, lines } = rootSourceLines(content);
61
+ const start = providerTableStart(lines.map(line => line.structural ? line.text : ""), id);
62
+ if (start < 0) throw new Error("Routing consent requires an explicit provider table.");
63
+ let end = start + 1;
64
+ while (end < lines.length && !(lines[end]!.structural && /^\s*\[/.test(lines[end]!.text))) end++;
65
+ for (let i = start + 1; i < end; i++) {
66
+ const field = sourceAssignment(lines, i, end);
67
+ if (field?.key !== "base_url") continue;
68
+ if (typeof field.value !== "string") break;
69
+ const url = new URL(field.value);
70
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) break;
71
+ return { url: field.value, assignment: field.text, replace: (text: string) =>
72
+ bom + sourceText(lines.slice(0, field.start)) + text + lines[field.end - 1]!.eol + sourceText(lines.slice(field.end)) };
73
+ }
74
+ throw new Error("Routing consent requires a credential-free HTTP(S) base_url assignment.");
75
+ }
76
+
77
+ function inWriteSection<T>(run: (held: LockHandle, content: string, record: RoutingRecord) => T): T {
78
+ assertCodexHomeOwner(getCodexHome());
79
+ const result = withConfigWriteLockHeld(CODEX_CONFIG_PATH, undefined, held => {
80
+ watchConfigWriteTargets(held, [CODEX_CONFIG_PATH, recordPath(), JOURNAL_PATH]);
81
+ const content = readFileSync(CODEX_CONFIG_PATH, "utf8");
82
+ return run(held, content, readRecord());
83
+ });
84
+ if (!result.ok) throw new ConfigWriteLockRefusal(result);
85
+ return result.value;
86
+ }
87
+
88
+ function publish(path: string, content: string, held: LockHandle): void {
89
+ publishConfigWriteTarget(CODEX_CONFIG_PATH, held, path, (destination, hooks) => atomicWriteFile(destination, content, undefined, hooks));
90
+ }
91
+
92
+ function save(record: RoutingRecord, held: LockHandle): void {
93
+ publish(recordPath(), JSON.stringify(record, null, 2) + "\n", held);
94
+ }
95
+
96
+ /** Read-only ownership evidence: consent alone never claims a URL we did not publish. */
97
+ export function isApprovedCodexProviderUrl(id: string, url: string): boolean {
98
+ const record = readRecord();
99
+ return Object.hasOwn(record.providers, id) && record.providers[id]!.managedUrls.includes(url);
100
+ }
101
+
102
+ export function approvedCodexProviders(): string[] {
103
+ return Object.keys(readRecord().providers);
104
+ }
105
+
106
+ /** Capture the exact source before asking; commit refuses a changed provider after the prompt. */
107
+ export function previewCodexProviderConsent(id: string): string {
108
+ return providerUrl(readFileSync(CODEX_CONFIG_PATH, "utf8"), id).assignment;
109
+ }
110
+
111
+ export function allowCodexProviderRouting(id: string, expected: string): void {
112
+ inWriteSection((held, content, record) => {
113
+ const field = providerUrl(content, id);
114
+ if (field.assignment !== expected) throw new Error("Provider changed after confirmation; review it and retry.");
115
+ if (Object.hasOwn(record.providers, id)) throw new Error("Provider is already approved; revoke it before granting new consent.");
116
+ record.providers[id] = { original: field.assignment, originalUrl: field.url, managedUrls: [] };
117
+ save(record, held);
118
+ });
119
+ }
120
+
121
+ function accepts(fieldUrl: string, saved: Approval): boolean {
122
+ return fieldUrl === saved.originalUrl || saved.managedUrls.includes(fieldUrl);
123
+ }
124
+
125
+ /** Called by start/sync, never prompts and never broadens the approved provider set. */
126
+ export function injectApprovedCodexProvider(id: string, target: CodexRoutingTarget, beforeWrite?: () => void, validateOnly = false): false | { configChanged: boolean } {
127
+ return inWriteSection((held, content, record) => {
128
+ if (!Object.hasOwn(record.providers, id)) return false;
129
+ const apply = () => {
130
+ target = validateCodexRoutingTarget(target);
131
+ if (target.requiresAdmissionToken || target.desktopAuthless || target.clientCompaction || !isLoopbackHostname(new URL(target.baseUrl).hostname)) {
132
+ throw new Error("Approved custom providers support only authenticated Codex on a local loopback proxy without client compaction.");
133
+ }
134
+ if (externalCodexModelProvider(content) !== id) throw new Error("Active provider changed; routing was not applied.");
135
+ const field = providerUrl(content, id), saved = record.providers[id]!;
136
+ if (!accepts(field.url, saved)) throw new Error("Approved provider base_url changed outside OpenCodex; revoke and approve it again.");
137
+ if (validateOnly) return { configChanged: false };
138
+ beforeWrite?.();
139
+ if (!shouldSyncCodexOnStart(loadConfig())) return false;
140
+ if (readFileSync(CODEX_CONFIG_PATH, "utf8") !== content) throw new Error("Codex config changed before publication; retry sync.");
141
+ publishConfigWriteTarget(CODEX_CONFIG_PATH, held, JOURNAL_PATH, (_destination, hooks) => removeJournal(hooks));
142
+ if (field.url === target.baseUrl) return { configChanged: false };
143
+ // Write recovery evidence first. Both the previous and candidate URL remain restorable
144
+ // if publication fails or the process exits between these two atomic renames.
145
+ saved.managedUrls = [...new Set([field.url, target.baseUrl])].filter(url => url !== saved.originalUrl);
146
+ save(record, held);
147
+ beforePublishForTests?.();
148
+ publish(CODEX_CONFIG_PATH, field.replace(`base_url = ${tomlString(target.baseUrl)}`), held);
149
+ return { configChanged: true };
150
+ };
151
+ return validateOnly ? apply() : withConfigMutationLockSync(apply);
152
+ });
153
+ }
154
+
155
+ /** Restore only our URLs; unrelated edits and provider IDs/history are never rewritten. */
156
+ export function restoreApprovedCodexProviders(revokeId?: string): boolean {
157
+ const ids = approvedCodexProviders();
158
+ if (revokeId !== undefined && !ids.includes(revokeId)) throw new Error("Provider has no routing consent to revoke.");
159
+ if (!ids.length) return false;
160
+ return inWriteSection((held, content, record) => {
161
+ let next = content;
162
+ for (const id of revokeId === undefined ? Object.keys(record.providers) : [revokeId]) {
163
+ const saved = record.providers[id]!;
164
+ const parsed = Bun.TOML.parse(next) as Record<string, unknown>;
165
+ const table = (parsed.model_providers as Record<string, Record<string, unknown>> | undefined)?.[id];
166
+ if (!table || !saved.managedUrls.includes(table.base_url as string)) continue;
167
+ const field = providerUrl(next, id, false);
168
+ if (saved.managedUrls.includes(field.url)) next = field.replace(saved.original);
169
+ }
170
+ if (next !== content) publish(CODEX_CONFIG_PATH, next, held);
171
+ if (revokeId !== undefined) { delete record.providers[revokeId]; save(record, held); }
172
+ return next !== content;
173
+ });
174
+ }
@@ -1,12 +1,12 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { invalidateCodexModelsCache, syncCatalogModels } from "./catalog";
3
+ import { invalidateCodexModelsCache, syncCatalogModels } from "./catalog/retained-sync";
4
4
  import type { ComboCatalogOmission } from "./catalog/aggregation";
5
5
  import { getCodexHome } from "./paths";
6
6
  import { withCatalogWriteSerialization, CatalogWritePermitRefusal } from "./catalog-write-serialization";
7
7
  import { replaceCodexModelsCache } from "./internal/catalog-writer";
8
8
  import type { OcxConfig } from "../types";
9
- import type { CodexCatalogSyncOptions } from "./catalog/sync";
9
+ import type { CodexCatalogSyncOptions } from "./catalog/retained-sync";
10
10
 
11
11
  export interface CodexCatalogRefreshResult {
12
12
  added: number;
@@ -1,6 +1,6 @@
1
1
  import { isCodexAccountGenerationLive } from "../account-store";
2
2
  import { NATIVE_RESERVE_MODEL } from "../catalog/native-models";
3
- import { MAIN_CODEX_ACCOUNT_ID } from "../main-account";
3
+ import { MAIN_CODEX_ACCOUNT_ID } from "../account-id";
4
4
  import { POOL_KEY_CODEX } from "../pool-rotation";
5
5
  import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
6
6
  import type { OcxConfig } from "../../types";
@@ -163,7 +163,7 @@ export function deleteScopedHealth(accountId: string, scope: CodexQuotaScope): v
163
163
  }
164
164
 
165
165
  /** Live quota-refusal avoidance for an account, including the lane the request belongs to. */
166
- function codexQuotaAvoidUntil(
166
+ export function codexQuotaAvoidUntil(
167
167
  accountId: string,
168
168
  quotaScope: CodexQuotaScope | undefined,
169
169
  now: number,