@iislee/opencodex 2.11.0 → 2.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (733) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/README.md +117 -22
  3. package/bin/ocx.mjs +245 -88
  4. package/bin/package-main.mjs +1 -1
  5. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  6. package/gui/dist/assets/index-D4OKgUV4.js +114 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  9. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  10. package/gui/dist/provider-icons/claude-color.svg +1 -1
  11. package/gui/dist/provider-icons/cline-color.svg +16 -0
  12. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  13. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  14. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  15. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  16. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  17. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  18. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  19. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  20. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  21. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  22. package/gui/dist/provider-icons/grok.svg +1 -1
  23. package/gui/dist/provider-icons/groq-color.svg +1 -1
  24. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  25. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  26. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  27. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  28. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  29. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  30. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  31. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  32. package/gui/dist/provider-icons/openai.svg +1 -1
  33. package/gui/dist/provider-icons/opencode.svg +2 -1
  34. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  35. package/gui/dist/provider-icons/pi.svg +2 -2
  36. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  37. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  38. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  39. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  40. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  41. package/package.json +21 -10
  42. package/src/adapters/anthropic-output-schema.ts +137 -0
  43. package/src/adapters/anthropic.ts +475 -62
  44. package/src/adapters/base.ts +82 -7
  45. package/src/adapters/client-fingerprint.ts +18 -12
  46. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  47. package/src/adapters/command-code.ts +637 -0
  48. package/src/adapters/cursor/call-id.ts +44 -0
  49. package/src/adapters/cursor/catalog.ts +541 -0
  50. package/src/adapters/cursor/checkpoint-store.ts +308 -0
  51. package/src/adapters/cursor/cursor-errors.ts +144 -6
  52. package/src/adapters/cursor/discovery.ts +122 -14
  53. package/src/adapters/cursor/effort-map.ts +106 -4
  54. package/src/adapters/cursor/envelope-echo.ts +290 -0
  55. package/src/adapters/cursor/framing.ts +39 -0
  56. package/src/adapters/cursor/h2-pool.ts +123 -0
  57. package/src/adapters/cursor/http1-bidi.ts +361 -0
  58. package/src/adapters/cursor/images.ts +704 -0
  59. package/src/adapters/cursor/live-models.ts +180 -59
  60. package/src/adapters/cursor/live-transport.ts +623 -170
  61. package/src/adapters/cursor/message-mapper.ts +4 -1
  62. package/src/adapters/cursor/native-exec-common.ts +23 -2
  63. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  64. package/src/adapters/cursor/native-exec-fs.ts +10 -7
  65. package/src/adapters/cursor/native-exec-network.ts +1 -1
  66. package/src/adapters/cursor/native-exec-shell.ts +5 -3
  67. package/src/adapters/cursor/native-exec.ts +121 -14
  68. package/src/adapters/cursor/protobuf-events.ts +829 -11
  69. package/src/adapters/cursor/protobuf-request.ts +447 -75
  70. package/src/adapters/cursor/request-builder.ts +271 -35
  71. package/src/adapters/cursor/tool-definitions.ts +252 -12
  72. package/src/adapters/cursor/tool-result-normalize.ts +115 -0
  73. package/src/adapters/cursor/transport.ts +22 -0
  74. package/src/adapters/cursor/types.ts +48 -1
  75. package/src/adapters/cursor.ts +336 -39
  76. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  77. package/src/adapters/google-antigravity-replay.ts +713 -41
  78. package/src/adapters/google-antigravity-wire.ts +48 -10
  79. package/src/adapters/google-errors.ts +44 -12
  80. package/src/adapters/google-http.ts +38 -10
  81. package/src/adapters/google-tool-schema.ts +80 -15
  82. package/src/adapters/google-truncation.ts +11 -0
  83. package/src/adapters/google.ts +742 -86
  84. package/src/adapters/identity.ts +39 -6
  85. package/src/adapters/image.ts +1 -1
  86. package/src/adapters/kiro-constants.ts +27 -0
  87. package/src/adapters/kiro-errors.ts +11 -0
  88. package/src/adapters/kiro-events.ts +19 -1
  89. package/src/adapters/kiro-thinking.ts +18 -2
  90. package/src/adapters/kiro-tools.ts +65 -17
  91. package/src/adapters/kiro.ts +270 -73
  92. package/src/adapters/mimo-free.ts +17 -0
  93. package/src/adapters/openai-chat-url.ts +11 -0
  94. package/src/adapters/openai-chat.ts +1377 -344
  95. package/src/adapters/openai-responses-url.ts +16 -0
  96. package/src/adapters/openai-responses.ts +1164 -64
  97. package/src/adapters/registry.ts +175 -0
  98. package/src/adapters/responses-tool-schema.ts +67 -0
  99. package/src/adapters/run-turn-queue.ts +36 -1
  100. package/src/adapters/tool-call-id.ts +119 -0
  101. package/src/adapters/tool-catalog-nudge.ts +105 -21
  102. package/src/adapters/xai-tool-schema.ts +436 -0
  103. package/src/adapters/xai-web-search.ts +186 -0
  104. package/src/bridge.ts +512 -92
  105. package/src/chat/inbound.ts +57 -20
  106. package/src/chat/outbound.ts +97 -33
  107. package/src/claude/agents-inject.ts +40 -10
  108. package/src/claude/context-windows.ts +37 -14
  109. package/src/claude/desktop-3p.ts +243 -9
  110. package/src/claude/gateway-cache.ts +41 -4
  111. package/src/claude/inbound.ts +72 -3
  112. package/src/claude/model-info.ts +38 -15
  113. package/src/claude/outbound.ts +88 -21
  114. package/src/cli/access.ts +46 -3
  115. package/src/cli/account-api.ts +150 -19
  116. package/src/cli/account-auth.ts +33 -6
  117. package/src/cli/account-catalog-refresh.ts +14 -0
  118. package/src/cli/account-extended.ts +648 -28
  119. package/src/cli/account-main.ts +317 -0
  120. package/src/cli/account.ts +97 -19
  121. package/src/cli/agent.ts +53 -2
  122. package/src/cli/alias.ts +66 -0
  123. package/src/cli/capabilities-command.ts +94 -0
  124. package/src/cli/capabilities.ts +496 -0
  125. package/src/cli/claude-agent-startup-sync.ts +73 -0
  126. package/src/cli/claude-desktop.ts +60 -15
  127. package/src/cli/claude.ts +129 -35
  128. package/src/cli/codex-log-guard-doctor.ts +103 -0
  129. package/src/cli/codex-shim-autorestore.ts +2 -0
  130. package/src/cli/codex-shim-readiness.ts +76 -0
  131. package/src/cli/combo.ts +8 -0
  132. package/src/cli/config-command.ts +74 -10
  133. package/src/cli/dispatch.ts +821 -0
  134. package/src/cli/doctor.ts +440 -56
  135. package/src/cli/ensure-desired-integrations.ts +152 -0
  136. package/src/cli/export-command.ts +46 -20
  137. package/src/cli/help.ts +34 -274
  138. package/src/cli/index.ts +417 -528
  139. package/src/cli/init.ts +5 -17
  140. package/src/cli/inspect.ts +230 -0
  141. package/src/cli/integrations.ts +120 -2
  142. package/src/cli/lab.ts +607 -0
  143. package/src/cli/launcher-context.ts +77 -0
  144. package/src/cli/minimax.ts +497 -0
  145. package/src/cli/models-runtime.ts +130 -2
  146. package/src/cli/models.ts +118 -16
  147. package/src/cli/observe.ts +123 -11
  148. package/src/cli/opencode.ts +4 -2
  149. package/src/cli/provider-runtime.ts +34 -3
  150. package/src/cli/provider.ts +25 -3
  151. package/src/cli/ready.ts +301 -0
  152. package/src/cli/registry.ts +476 -0
  153. package/src/cli/root.ts +86 -0
  154. package/src/cli/route-policy.ts +92 -0
  155. package/src/cli/runtime-api.ts +57 -10
  156. package/src/cli/star-prompt.ts +71 -15
  157. package/src/cli/status.ts +51 -4
  158. package/src/cli/storage.ts +234 -0
  159. package/src/cli/system-command.ts +16 -0
  160. package/src/cli/system-restart-client.ts +146 -0
  161. package/src/cli/tray-proxy.ts +153 -6
  162. package/src/cli/usage-report.ts +184 -0
  163. package/src/cli/v2.ts +137 -18
  164. package/src/cli/version-skew.ts +46 -0
  165. package/src/cli.ts +1 -1
  166. package/src/clients/config-export.ts +1442 -23
  167. package/src/clients/effective-status.ts +30 -2
  168. package/src/clients/probes/cline.ts +81 -0
  169. package/src/codex/account-label.ts +35 -1
  170. package/src/codex/account-lifecycle.ts +130 -13
  171. package/src/codex/account-namespaces.ts +49 -3
  172. package/src/codex/account-pause.ts +2 -1
  173. package/src/codex/account-priority.ts +84 -0
  174. package/src/codex/account-store.ts +29 -2
  175. package/src/codex/account-usability.ts +25 -2
  176. package/src/codex/admission.ts +256 -0
  177. package/src/codex/affinity-debug.ts +162 -0
  178. package/src/codex/app-server-processes.ts +570 -107
  179. package/src/codex/app-server-restart-service.ts +232 -0
  180. package/src/codex/auth-api.ts +893 -246
  181. package/src/codex/auth-collision.ts +5 -3
  182. package/src/codex/auth-context.ts +348 -32
  183. package/src/codex/autostart-health.ts +24 -1
  184. package/src/codex/catalog/account-models.ts +67 -0
  185. package/src/codex/catalog/aggregation.ts +86 -10
  186. package/src/codex/catalog/bundled.ts +331 -33
  187. package/src/codex/catalog/effort.ts +138 -32
  188. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  189. package/src/codex/catalog/kinds.ts +2 -0
  190. package/src/codex/catalog/metadata.ts +555 -45
  191. package/src/codex/catalog/native-models.ts +75 -0
  192. package/src/codex/catalog/parsing.ts +269 -30
  193. package/src/codex/catalog/provider-fetch.ts +1626 -144
  194. package/src/codex/catalog/sync.ts +1576 -187
  195. package/src/codex/catalog-admission.ts +199 -0
  196. package/src/codex/catalog-refresh-status.ts +105 -0
  197. package/src/codex/catalog-write-serialization.ts +242 -0
  198. package/src/codex/catalog.ts +6 -3
  199. package/src/codex/codex-write-lock.ts +386 -0
  200. package/src/codex/convergence-types.ts +614 -0
  201. package/src/codex/convergence.ts +676 -0
  202. package/src/codex/coordinator-doctor.ts +332 -0
  203. package/src/codex/custom-model-catalog-migration.ts +176 -0
  204. package/src/codex/desired-state.ts +230 -0
  205. package/src/codex/desktop-app-restart.ts +355 -0
  206. package/src/codex/features.ts +636 -39
  207. package/src/codex/generation.ts +202 -0
  208. package/src/codex/history-job.ts +436 -0
  209. package/src/codex/history-lock.ts +242 -0
  210. package/src/codex/history-manifest.ts +112 -0
  211. package/src/codex/history-migration-guardian.ts +30 -24
  212. package/src/codex/history-provider.ts +1016 -235
  213. package/src/codex/history-transition.ts +105 -0
  214. package/src/codex/history-worker.ts +223 -0
  215. package/src/codex/inject-coordination.ts +373 -0
  216. package/src/codex/inject.ts +1114 -152
  217. package/src/codex/injected-marker.ts +37 -3
  218. package/src/codex/integration-record.ts +266 -0
  219. package/src/codex/internal/catalog-writer.ts +203 -0
  220. package/src/codex/internal/history-writer.ts +81 -0
  221. package/src/codex/journal.ts +66 -4
  222. package/src/codex/log-guard/inspect.ts +524 -0
  223. package/src/codex/log-guard/lock.ts +150 -0
  224. package/src/codex/log-guard/maintenance.ts +403 -0
  225. package/src/codex/log-guard/path-safety.ts +88 -0
  226. package/src/codex/log-guard/policy.ts +44 -0
  227. package/src/codex/log-guard/processes.ts +205 -0
  228. package/src/codex/log-guard/protection.ts +489 -0
  229. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  230. package/src/codex/main-account-cache.ts +24 -0
  231. package/src/codex/main-account.ts +29 -1
  232. package/src/codex/management-convergence.ts +167 -0
  233. package/src/codex/model-cache.ts +56 -10
  234. package/src/codex/model-entitlements.ts +360 -0
  235. package/src/codex/native-main-admission.ts +47 -0
  236. package/src/codex/native-main-auth-temp.ts +187 -0
  237. package/src/codex/native-main-claim.ts +178 -0
  238. package/src/codex/native-main-lock-file.ts +162 -0
  239. package/src/codex/native-main-owner.ts +329 -0
  240. package/src/codex/native-profile-api.ts +247 -0
  241. package/src/codex/native-profile-manager.ts +1531 -0
  242. package/src/codex/native-profile-processes.ts +121 -0
  243. package/src/codex/native-profile-recovery.ts +99 -0
  244. package/src/codex/native-profile-stage-store.ts +387 -0
  245. package/src/codex/native-profile-startup.ts +622 -0
  246. package/src/codex/native-profile-store.ts +855 -0
  247. package/src/codex/native-profile-types.ts +120 -0
  248. package/src/codex/native-residue.ts +675 -0
  249. package/src/codex/paths.ts +80 -1
  250. package/src/codex/plan-from-token.ts +140 -0
  251. package/src/codex/plan.ts +40 -0
  252. package/src/codex/plugins-doctor.ts +1 -1
  253. package/src/codex/pool-rotation.ts +74 -4
  254. package/src/codex/project-config-warnings.ts +20 -6
  255. package/src/codex/prompt-journal.ts +352 -0
  256. package/src/codex/prompt-layers.ts +1313 -0
  257. package/src/codex/prompt-lock.ts +143 -0
  258. package/src/codex/prompt-text-probe.ts +238 -0
  259. package/src/codex/quota-rejection.ts +298 -0
  260. package/src/codex/quota.ts +264 -21
  261. package/src/codex/refresh.ts +11 -2
  262. package/src/codex/reset-credit-recovery.ts +1044 -0
  263. package/src/codex/routing.ts +514 -94
  264. package/src/codex/runtime.ts +159 -38
  265. package/src/codex/shim.ts +1065 -31
  266. package/src/codex/subagent-model-fallback.ts +437 -43
  267. package/src/codex/sync.ts +191 -2
  268. package/src/codex/transition-state.ts +720 -0
  269. package/src/codex/upstream-host-health.ts +368 -0
  270. package/src/codex/user-identity.ts +557 -0
  271. package/src/codex/warmup.ts +187 -81
  272. package/src/codex/write-coordination.ts +114 -0
  273. package/src/combos/failover.ts +47 -0
  274. package/src/combos/index.ts +4 -0
  275. package/src/combos/request.ts +32 -0
  276. package/src/combos/types.ts +91 -9
  277. package/src/compatibility/index.ts +26 -0
  278. package/src/compatibility/manifest.ts +253 -0
  279. package/src/compatibility/openai-responses.ts +121 -0
  280. package/src/config/atomic-write.ts +219 -0
  281. package/src/config/paths.ts +40 -0
  282. package/src/config/process-state.ts +309 -0
  283. package/src/config/provider-name.ts +24 -0
  284. package/src/config/provider-validation.ts +177 -0
  285. package/src/config/rebase-provenance.ts +68 -0
  286. package/src/config.ts +1713 -709
  287. package/src/generated/compatibility-version.json +3324 -0
  288. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
  289. package/src/grok/inject.ts +16 -5
  290. package/src/grok/inspect.ts +45 -0
  291. package/src/grok/sync.ts +2 -2
  292. package/src/images/loop.ts +157 -33
  293. package/src/images/plan.ts +24 -13
  294. package/src/integrations/config-io.ts +269 -0
  295. package/src/integrations/journal.ts +315 -0
  296. package/src/integrations/merge.ts +135 -0
  297. package/src/integrations/mutation-flight.ts +71 -0
  298. package/src/integrations/native/ownership-preflight.ts +202 -0
  299. package/src/integrations/omp-yaml-source.ts +358 -0
  300. package/src/integrations/owned-refresh.ts +74 -0
  301. package/src/integrations/ownership-policy.ts +160 -0
  302. package/src/integrations/ownership.ts +155 -0
  303. package/src/integrations/registry.ts +166 -0
  304. package/src/integrations/serialize.ts +314 -0
  305. package/src/integrations/state.ts +433 -0
  306. package/src/integrations/store.ts +103 -0
  307. package/src/integrations/writer-lock.ts +98 -0
  308. package/src/integrations/writer.ts +715 -0
  309. package/src/lab/artifacts/sanitize.ts +586 -0
  310. package/src/lab/artifacts/secure-fs.ts +475 -0
  311. package/src/lab/artifacts/store.ts +310 -0
  312. package/src/lab/automation/budgets.ts +78 -0
  313. package/src/lab/automation/config-persistence.ts +256 -0
  314. package/src/lab/automation/constants.ts +39 -0
  315. package/src/lab/automation/cooldown.ts +103 -0
  316. package/src/lab/automation/dispatch.ts +211 -0
  317. package/src/lab/automation/index.ts +13 -0
  318. package/src/lab/automation/orchestrator.ts +499 -0
  319. package/src/lab/automation/persistence.ts +512 -0
  320. package/src/lab/automation/planner.ts +371 -0
  321. package/src/lab/automation/policy.ts +136 -0
  322. package/src/lab/automation/queue.ts +191 -0
  323. package/src/lab/automation/recovery.ts +24 -0
  324. package/src/lab/automation/route-context.ts +21 -0
  325. package/src/lab/automation/run-key.ts +44 -0
  326. package/src/lab/automation/runs-query.ts +34 -0
  327. package/src/lab/automation/types.ts +160 -0
  328. package/src/lab/conformance/assertion.ts +325 -0
  329. package/src/lab/conformance/digest.ts +22 -0
  330. package/src/lab/conformance/executor.ts +741 -0
  331. package/src/lab/conformance/fixture-provider.ts +27 -0
  332. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  333. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  334. package/src/lab/conformance/harness-budget.ts +47 -0
  335. package/src/lab/conformance/index.ts +5 -0
  336. package/src/lab/conformance/jcs.ts +64 -0
  337. package/src/lab/conformance/json-pointer.ts +39 -0
  338. package/src/lab/conformance/manifest.ts +180 -0
  339. package/src/lab/conformance/mcp-stub.ts +179 -0
  340. package/src/lab/conformance/negative-controls.ts +164 -0
  341. package/src/lab/conformance/observation.ts +355 -0
  342. package/src/lab/conformance/runner.ts +68 -0
  343. package/src/lab/conformance/sse-normalize.ts +59 -0
  344. package/src/lab/conformance/suite-manifest.ts +78 -0
  345. package/src/lab/conformance/types.ts +214 -0
  346. package/src/lab/constants.ts +126 -0
  347. package/src/lab/digest.ts +64 -0
  348. package/src/lab/events/errors.ts +9 -0
  349. package/src/lab/events/limits.ts +117 -0
  350. package/src/lab/events/types.ts +229 -0
  351. package/src/lab/events/validate.ts +781 -0
  352. package/src/lab/fabric/constants.ts +40 -0
  353. package/src/lab/fabric/executor.ts +492 -0
  354. package/src/lab/fabric/index.ts +80 -0
  355. package/src/lab/fabric/manifest.ts +222 -0
  356. package/src/lab/fabric/observe.ts +489 -0
  357. package/src/lab/fabric/patch.ts +79 -0
  358. package/src/lab/fabric/producer-child.ts +139 -0
  359. package/src/lab/fabric/producer-isolate.ts +276 -0
  360. package/src/lab/fabric/producer-protocol.ts +61 -0
  361. package/src/lab/fabric/scratch.ts +439 -0
  362. package/src/lab/fabric/subject.ts +106 -0
  363. package/src/lab/fabric/types.ts +134 -0
  364. package/src/lab/fabric/verifier.ts +98 -0
  365. package/src/lab/index.ts +54 -0
  366. package/src/lab/ledger/artifact-refs.ts +127 -0
  367. package/src/lab/ledger/invalidation.ts +136 -0
  368. package/src/lab/ledger/purge.ts +310 -0
  369. package/src/lab/ledger/store.ts +532 -0
  370. package/src/lab/live/credential-lease.ts +53 -0
  371. package/src/lab/live/destination.ts +155 -0
  372. package/src/lab/live/executor.ts +336 -0
  373. package/src/lab/live/inert-tools.ts +56 -0
  374. package/src/lab/live/manifest.ts +85 -0
  375. package/src/lab/live/mcp-loopback.ts +57 -0
  376. package/src/lab/live/runner.ts +19 -0
  377. package/src/lab/live/sandbox.ts +61 -0
  378. package/src/lab/live/suite-manifest.ts +41 -0
  379. package/src/lab/live/transport.ts +118 -0
  380. package/src/lab/live/types.ts +197 -0
  381. package/src/lab/observe/from-conformance.ts +301 -0
  382. package/src/lab/observe/from-live.ts +117 -0
  383. package/src/lab/paths.ts +153 -0
  384. package/src/lab/projection/rebuild.ts +495 -0
  385. package/src/lab/projection/schema.ts +135 -0
  386. package/src/lab/projection/verdicts.ts +474 -0
  387. package/src/lab/projection/verification.ts +412 -0
  388. package/src/lab/public/bundle.ts +217 -0
  389. package/src/lab/public/community-authority.ts +175 -0
  390. package/src/lab/public/community-files.ts +29 -0
  391. package/src/lab/public/community.ts +479 -0
  392. package/src/lab/public/file-safety.ts +155 -0
  393. package/src/lab/public/ids.ts +26 -0
  394. package/src/lab/public/index.ts +16 -0
  395. package/src/lab/public/mutation-lock.ts +424 -0
  396. package/src/lab/public/operator.ts +353 -0
  397. package/src/lab/public/origin-purge.ts +79 -0
  398. package/src/lab/public/origin.ts +203 -0
  399. package/src/lab/public/privacy.ts +143 -0
  400. package/src/lab/public/private-file.ts +261 -0
  401. package/src/lab/public/project.ts +124 -0
  402. package/src/lab/public/purge-test-fault.ts +21 -0
  403. package/src/lab/public/purge.ts +223 -0
  404. package/src/lab/public/registry.ts +44 -0
  405. package/src/lab/public/revocation.ts +252 -0
  406. package/src/lab/public/signature.ts +243 -0
  407. package/src/lab/public/storage.ts +105 -0
  408. package/src/lab/public/strict-json.ts +206 -0
  409. package/src/lab/public/time.ts +26 -0
  410. package/src/lab/public/types.ts +172 -0
  411. package/src/lab/public/validate.ts +391 -0
  412. package/src/lab/query/catalog.ts +101 -0
  413. package/src/lab/query/connection.ts +107 -0
  414. package/src/lab/query/constants.ts +4 -0
  415. package/src/lab/query/cursor.ts +132 -0
  416. package/src/lab/query/dto-map.ts +277 -0
  417. package/src/lab/query/errors.ts +22 -0
  418. package/src/lab/query/freshness.ts +53 -0
  419. package/src/lab/query/index.ts +45 -0
  420. package/src/lab/query/latest-observation.ts +59 -0
  421. package/src/lab/query/passive-production.ts +159 -0
  422. package/src/lab/query/queries.ts +444 -0
  423. package/src/lab/query/types.ts +266 -0
  424. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  425. package/src/lab/subject/installation-salt.ts +112 -0
  426. package/src/lab/subject/protocol-subject.ts +80 -0
  427. package/src/lab/subject/route-subject.ts +74 -0
  428. package/src/lib/admin-secrets.ts +24 -0
  429. package/src/lib/app-owned-memory-stores.ts +22 -0
  430. package/src/lib/bounded-body.ts +167 -11
  431. package/src/lib/bun-runtime.ts +125 -12
  432. package/src/lib/bun-stream-caps.ts +13 -9
  433. package/src/lib/codex-restart-contract.ts +120 -0
  434. package/src/lib/config-ownership.ts +26 -2
  435. package/src/lib/destination-policy.ts +65 -1
  436. package/src/lib/errors.ts +80 -5
  437. package/src/lib/fabric-task-execution-authority.ts +7 -0
  438. package/src/lib/fabric-task-host.ts +29 -0
  439. package/src/lib/lab-activation.ts +223 -0
  440. package/src/lib/lab-live-execution-authority.ts +13 -0
  441. package/src/lib/lab-live-host.ts +30 -0
  442. package/src/lib/lab-live-pinned-sender.ts +56 -0
  443. package/src/lib/lab-live-route-production.ts +130 -0
  444. package/src/lib/lab-passive-linker-registration.ts +26 -0
  445. package/src/lib/local-management-attestation.ts +51 -0
  446. package/src/lib/local-management-capability.ts +100 -0
  447. package/src/lib/local-provider-reload-contract.ts +100 -0
  448. package/src/lib/optional-shutdown-hooks.ts +57 -0
  449. package/src/lib/package-tree-integrity.ts +101 -0
  450. package/src/lib/pinned-http.ts +145 -26
  451. package/src/lib/process-control.ts +6 -2
  452. package/src/lib/provider-outbound.ts +49 -9
  453. package/src/lib/redact.ts +419 -3
  454. package/src/lib/self-launch-argv.ts +15 -0
  455. package/src/lib/server-resource-ownership.ts +71 -0
  456. package/src/lib/service-secrets.ts +15 -0
  457. package/src/lib/shadow-call.ts +35 -4
  458. package/src/lib/sse-decoder.ts +41 -0
  459. package/src/lib/state-store-registrations.ts +10 -2
  460. package/src/lib/system-restart-contract.ts +73 -0
  461. package/src/lib/token-estimate.ts +19 -2
  462. package/src/lib/tool-argument-integers.ts +253 -0
  463. package/src/lib/translator-budget.ts +44 -0
  464. package/src/lib/upstream-http-version.ts +57 -0
  465. package/src/lib/upstream-reachability.ts +95 -0
  466. package/src/lib/upstream-retry.ts +156 -3
  467. package/src/lib/windows-atomic-replace.ts +155 -0
  468. package/src/lib/windows-elevation.ts +70 -2
  469. package/src/lib/windows-secret-acl.ts +409 -69
  470. package/src/lib/windows-service-wrappers.ts +72 -0
  471. package/src/lib/windows-text.ts +106 -0
  472. package/src/lib/windows-user-principal.ts +341 -0
  473. package/src/lib/winsw.ts +33 -5
  474. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  475. package/src/oauth/account-import/index.ts +15 -0
  476. package/src/oauth/account-import/parser.ts +83 -0
  477. package/src/oauth/account-import/registry.ts +18 -0
  478. package/src/oauth/account-import/service.ts +75 -0
  479. package/src/oauth/account-import/types.ts +91 -0
  480. package/src/oauth/anthropic.ts +12 -1
  481. package/src/oauth/callback-server.ts +30 -4
  482. package/src/oauth/chatgpt.ts +12 -1
  483. package/src/oauth/cline.ts +203 -0
  484. package/src/oauth/command-code.ts +239 -0
  485. package/src/oauth/cursor.ts +46 -5
  486. package/src/oauth/generic-account-failover.ts +231 -0
  487. package/src/oauth/google-antigravity.ts +35 -3
  488. package/src/oauth/health.ts +20 -12
  489. package/src/oauth/index.ts +462 -71
  490. package/src/oauth/key-providers.ts +25 -0
  491. package/src/oauth/kimi.ts +25 -3
  492. package/src/oauth/kiro.ts +95 -6
  493. package/src/oauth/local-token-detect.ts +13 -2
  494. package/src/oauth/log.ts +3 -1
  495. package/src/oauth/login-cli.ts +88 -28
  496. package/src/oauth/nous.ts +798 -0
  497. package/src/oauth/open-browser-choice.ts +26 -0
  498. package/src/oauth/store.ts +133 -26
  499. package/src/oauth/token-guardian.ts +9 -3
  500. package/src/oauth/types.ts +15 -0
  501. package/src/pi/models.ts +2 -2
  502. package/src/providers/alibaba-region-migration.ts +1 -1
  503. package/src/providers/antigravity-models.ts +521 -31
  504. package/src/providers/auto-compact-budget.ts +65 -0
  505. package/src/providers/base-url-choices.ts +10 -0
  506. package/src/providers/codex-capacity.ts +292 -0
  507. package/src/providers/command-code-efforts.ts +176 -0
  508. package/src/providers/context-cap.ts +26 -8
  509. package/src/providers/cursor-pool.ts +72 -0
  510. package/src/providers/default-aliases.ts +65 -0
  511. package/src/providers/derive.ts +281 -6
  512. package/src/providers/fastwire.ts +507 -0
  513. package/src/providers/free-directory.ts +10 -7
  514. package/src/providers/google-vertex-location.ts +14 -0
  515. package/src/providers/key-failover.ts +71 -3
  516. package/src/providers/label.ts +35 -2
  517. package/src/providers/model-discovery-limits.ts +16 -0
  518. package/src/providers/model-discovery.ts +115 -22
  519. package/src/providers/model-presets.ts +119 -0
  520. package/src/providers/model-rename-migration.ts +255 -0
  521. package/src/providers/model-rename-startup.ts +28 -0
  522. package/src/providers/new-model-policy.ts +146 -0
  523. package/src/providers/openai-sidecar.ts +72 -4
  524. package/src/providers/openai-tier-startup.ts +31 -2
  525. package/src/providers/openai-tiers.ts +119 -4
  526. package/src/providers/openai-virtual-models.ts +1 -0
  527. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  528. package/src/providers/provider-id-rewrite.ts +30 -0
  529. package/src/providers/quota.ts +1379 -40
  530. package/src/providers/registry.ts +1545 -112
  531. package/src/providers/request-pacing.ts +310 -0
  532. package/src/providers/service-tier.ts +277 -0
  533. package/src/providers/slug-codec.ts +94 -6
  534. package/src/providers/static-model-discovery.ts +86 -0
  535. package/src/providers/xai-responses-opt-in.ts +15 -0
  536. package/src/providers/xai-transport.ts +32 -4
  537. package/src/reasoning-effort.ts +68 -3
  538. package/src/responses/apply-patch-envelope.ts +63 -0
  539. package/src/responses/code-mode-helper-compat.ts +50 -0
  540. package/src/responses/compaction.ts +26 -1
  541. package/src/responses/custom-tool-compat.ts +384 -0
  542. package/src/responses/hosted-tool-policy.ts +9 -0
  543. package/src/responses/namespace-tool-compat.ts +435 -0
  544. package/src/responses/parser.ts +225 -38
  545. package/src/responses/provider-continuation.ts +98 -0
  546. package/src/responses/provider-opaque-metadata.ts +73 -0
  547. package/src/responses/reasoning-envelope.ts +9 -1
  548. package/src/responses/reasoning-replay-cache.ts +504 -0
  549. package/src/responses/schema.ts +12 -2
  550. package/src/responses/spill-store.ts +75 -10
  551. package/src/responses/state.ts +659 -31
  552. package/src/responses/thought-signature-replay.ts +364 -0
  553. package/src/responses/tool-search-compat.ts +301 -0
  554. package/src/responses/truncated-stop-reason.ts +60 -0
  555. package/src/responses/turn-termination.ts +107 -0
  556. package/src/router.ts +405 -31
  557. package/src/routing/analytics.ts +379 -0
  558. package/src/routing/capability.ts +244 -0
  559. package/src/routing/compatibility/assemble.ts +73 -0
  560. package/src/routing/compatibility/behavior.ts +278 -0
  561. package/src/routing/compatibility/catalog.ts +99 -0
  562. package/src/routing/compatibility/endpoint.ts +52 -0
  563. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  564. package/src/routing/compatibility/policy.ts +181 -0
  565. package/src/routing/compatibility/provider-slot.ts +56 -0
  566. package/src/routing/compatibility/reader.ts +110 -0
  567. package/src/routing/compatibility/subject.ts +191 -0
  568. package/src/routing/compatibility/types.ts +64 -0
  569. package/src/routing/compatibility/version.ts +104 -0
  570. package/src/routing/cost.ts +77 -0
  571. package/src/routing/evaluator.ts +495 -0
  572. package/src/routing/health.ts +412 -0
  573. package/src/routing/history/cursor.ts +43 -0
  574. package/src/routing/history/indexer.ts +605 -0
  575. package/src/routing/history/schema.ts +72 -0
  576. package/src/routing/profile-namespace.ts +15 -0
  577. package/src/routing/profile.ts +547 -0
  578. package/src/routing/quota.ts +155 -0
  579. package/src/routing/request-evidence.ts +45 -0
  580. package/src/routing/trace.ts +776 -0
  581. package/src/server/adapter-resolve.ts +2 -29
  582. package/src/server/auth-cors.ts +289 -48
  583. package/src/server/background-lifecycle.ts +182 -0
  584. package/src/server/chat-completions.ts +154 -72
  585. package/src/server/chat-native-sse.ts +331 -0
  586. package/src/server/chat-native.ts +445 -0
  587. package/src/server/claude-messages.ts +160 -44
  588. package/src/server/direct-local-http.ts +347 -0
  589. package/src/server/effort-policy.ts +18 -0
  590. package/src/server/github-copilot-responses-repair.ts +338 -0
  591. package/src/server/gui-static.ts +39 -10
  592. package/src/server/images.ts +94 -12
  593. package/src/server/index.ts +1021 -190
  594. package/src/server/lifecycle.ts +336 -14
  595. package/src/server/live.ts +136 -17
  596. package/src/server/local-management-read-client.ts +90 -0
  597. package/src/server/local-provider-reload-client.ts +137 -0
  598. package/src/server/management/agent-settings-routes.ts +433 -116
  599. package/src/server/management/api-key-usage.ts +31 -5
  600. package/src/server/management/body.ts +6 -0
  601. package/src/server/management/codex-prompt-routes.ts +570 -0
  602. package/src/server/management/combo-routes.ts +64 -25
  603. package/src/server/management/config-routes.ts +511 -51
  604. package/src/server/management/context.ts +89 -2
  605. package/src/server/management/integration-routes.ts +498 -0
  606. package/src/server/management/lab-automation-routes.ts +206 -0
  607. package/src/server/management/lab-routes.ts +563 -0
  608. package/src/server/management/logs-usage-routes.ts +127 -52
  609. package/src/server/management/model-routes.ts +461 -133
  610. package/src/server/management/model-rows.ts +167 -0
  611. package/src/server/management/native-integration-routes.ts +770 -0
  612. package/src/server/management/oauth-account-routes.ts +90 -8
  613. package/src/server/management/provider-capability-config.ts +48 -0
  614. package/src/server/management/provider-routes.ts +836 -159
  615. package/src/server/management/request-history-routes.ts +191 -0
  616. package/src/server/management/route-registry.ts +332 -0
  617. package/src/server/management/routing-analytics-routes.ts +74 -0
  618. package/src/server/management/routing-profile-routes.ts +380 -0
  619. package/src/server/management/shared.ts +27 -11
  620. package/src/server/management/sidebar-routes.ts +47 -31
  621. package/src/server/management/storage-log-guard-routes.ts +186 -0
  622. package/src/server/management/sync-response.ts +69 -0
  623. package/src/server/management/system-restart.ts +289 -33
  624. package/src/server/management/system-routes.ts +77 -0
  625. package/src/server/management/usage-summary-cache.ts +9 -1
  626. package/src/server/management/vision-sidecar-options.ts +167 -0
  627. package/src/server/management/web-search-sidecar-options.ts +120 -0
  628. package/src/server/management-api.ts +126 -23
  629. package/src/server/management-auth.ts +220 -5
  630. package/src/server/passive-route-linker.ts +66 -0
  631. package/src/server/port-reclaim.ts +1 -1
  632. package/src/server/ports.ts +41 -1
  633. package/src/server/proxy-liveness.ts +161 -10
  634. package/src/server/readiness.ts +99 -0
  635. package/src/server/relay-eager.ts +221 -54
  636. package/src/server/relay.ts +479 -87
  637. package/src/server/request-decompress.ts +113 -6
  638. package/src/server/request-log-conversation.ts +51 -0
  639. package/src/server/request-log.ts +298 -31
  640. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  641. package/src/server/responses/agent-task-recovery.ts +465 -0
  642. package/src/server/responses/codex-auth-error.ts +55 -0
  643. package/src/server/responses/collaboration.ts +239 -36
  644. package/src/server/responses/combo-stream-preflight.ts +178 -0
  645. package/src/server/responses/compact.ts +444 -72
  646. package/src/server/responses/core.ts +3579 -436
  647. package/src/server/responses/empty-completion-guard.ts +333 -0
  648. package/src/server/responses/encrypted-payload.ts +62 -39
  649. package/src/server/responses/fetch-helpers.ts +89 -99
  650. package/src/server/responses/input-admission.ts +187 -0
  651. package/src/server/responses/pacing-overload.ts +13 -0
  652. package/src/server/responses/passthrough-error.ts +33 -9
  653. package/src/server/responses/policy-fallback.ts +178 -0
  654. package/src/server/responses/responses-field-backfill.ts +343 -0
  655. package/src/server/responses/terminal-guard.ts +26 -5
  656. package/src/server/responses/upstream-error.ts +5 -0
  657. package/src/server/responses/ws-upstream.ts +381 -0
  658. package/src/server/responses-custom-tool-repair.ts +363 -0
  659. package/src/server/responses-item-id-repair.ts +54 -6
  660. package/src/server/responses-json-events.ts +90 -0
  661. package/src/server/responses-model-rewrite.ts +29 -0
  662. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  663. package/src/server/responses-snapshot-repair.ts +621 -0
  664. package/src/server/responses-terminal-repair.ts +363 -0
  665. package/src/server/responses-tool-search-repair.ts +267 -0
  666. package/src/server/responses-undeclared-tool-guard.ts +376 -0
  667. package/src/server/responses.ts +18 -2
  668. package/src/server/search.ts +78 -13
  669. package/src/server/sse-frame-buffer.ts +319 -0
  670. package/src/server/sse-payload-rewrite.ts +110 -22
  671. package/src/server/startup-action-control.ts +8 -1
  672. package/src/server/startup-health-cache.ts +19 -1
  673. package/src/server/system-env.ts +80 -9
  674. package/src/server/ws-bridge.ts +53 -40
  675. package/src/service-manager-probe.ts +892 -0
  676. package/src/service.ts +1153 -94
  677. package/src/sidecar/auth.ts +92 -0
  678. package/src/sidecar/candidates.ts +83 -0
  679. package/src/storage/cleanup.ts +2 -2
  680. package/src/storage/policy-job.ts +14 -4
  681. package/src/storage/policy.ts +88 -23
  682. package/src/storage/scanner.ts +1 -1
  683. package/src/storage/worker-lifecycle.ts +14 -14
  684. package/src/tray/windows-tray.ps1 +83 -9
  685. package/src/tray/windows.ts +43 -16
  686. package/src/types/accounts.ts +37 -0
  687. package/src/types/config.ts +911 -0
  688. package/src/types/provider.ts +614 -0
  689. package/src/types/request.ts +397 -0
  690. package/src/types/tools.ts +236 -0
  691. package/src/types/wire.ts +80 -0
  692. package/src/types.ts +107 -1237
  693. package/src/update/index.ts +37 -23
  694. package/src/update/job.ts +445 -68
  695. package/src/update/notify.ts +12 -6
  696. package/src/update/npm-cache-preflight.d.mts +47 -0
  697. package/src/update/npm-cache-preflight.mjs +201 -0
  698. package/src/update/transactional-install.d.mts +22 -0
  699. package/src/update/transactional-install.mjs +266 -0
  700. package/src/usage/cost.ts +0 -0
  701. package/src/usage/expected-prices.ts +287 -16
  702. package/src/usage/log.ts +666 -50
  703. package/src/usage/summary.ts +388 -17
  704. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  705. package/src/usage/user-cost-overlays.ts +314 -0
  706. package/src/vision/anthropic-describe.ts +10 -6
  707. package/src/vision/backends.ts +97 -0
  708. package/src/vision/describe.ts +27 -16
  709. package/src/vision/eligibility.ts +250 -0
  710. package/src/vision/index.ts +238 -24
  711. package/src/vision/reasoning.ts +55 -0
  712. package/src/vision/routed-describe.ts +175 -0
  713. package/src/vision/timeout-bounds.ts +9 -0
  714. package/src/web-search/anthropic-executor.ts +13 -7
  715. package/src/web-search/backends.ts +108 -0
  716. package/src/web-search/exa-executor.ts +119 -0
  717. package/src/web-search/executor.ts +21 -6
  718. package/src/web-search/gemini-executor.ts +141 -0
  719. package/src/web-search/index.ts +150 -15
  720. package/src/web-search/loop.ts +284 -54
  721. package/src/web-search/parse.ts +125 -30
  722. package/src/web-search/sources.ts +60 -0
  723. package/src/web-search/xai-executor.ts +219 -0
  724. package/gui/dist/assets/index-DTpMHS4F.js +0 -67
  725. package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
  726. package/gui/dist/provider-icons/antigravity.svg +0 -1
  727. package/gui/dist/provider-icons/claude.svg +0 -1
  728. package/gui/dist/provider-icons/copilot.svg +0 -1
  729. package/gui/dist/provider-icons/cursor.svg +0 -2
  730. package/gui/dist/provider-icons/gemini.svg +0 -1
  731. package/gui/dist/provider-icons/grok-color.svg +0 -1
  732. package/gui/dist/provider-icons/kiro.svg +0 -14
  733. package/src/cli/internal-dispatch.ts +0 -20
@@ -1,4 +1,5 @@
1
1
  import { markActivity } from "../lib/sidecar-tracker";
2
+ import { knownModelIdsForProvider } from "../router";
2
3
  import {
3
4
  buildWarmupCompletionFrames,
4
5
  buildWsErrorFrame,
@@ -16,16 +17,25 @@ import {
16
17
  armClaudeCodeBaseline,
17
18
  loadConfig,
18
19
  saveConfig,
20
+ getConfigDir,
19
21
  websocketsEnabled,
20
22
  } from "../config";
21
23
  import { reconcileOAuthProviders } from "../oauth";
22
- import { invalidateCodexModelsCache } from "../codex/catalog";
23
- import { startMemoryWatchdog } from "./memory-watchdog";
24
+ import { withCatalogWriteSerialization } from "../codex/catalog-write-serialization";
25
+ import { invalidateCodexModelsCacheWithPermit } from "../codex/catalog/sync";
26
+ import { currentServiceHomes, serviceStatePathsForOpenCodexHome } from "../service";
27
+ import { shouldSyncCodexOnStart } from "../codex/desired-state";
28
+ import {
29
+ inspectNativeCodexOwnership,
30
+ type NativeCodexOwnership,
31
+ type OwnershipInspection,
32
+ } from "../integrations/native/ownership-preflight";
33
+ import { registerCodexCooldownRecoveryProbeWorker } from "../codex/auth-api";
24
34
  import {
25
35
  reconcileLiveStateStores,
26
36
  setLiveStateStoreConfig,
27
37
  } from "../lib/state-store-registrations";
28
- import { startStateStoreSweeper } from "../lib/state-store-sweeper";
38
+ import { startUserCostOverlayReconciler } from "../usage/user-cost-overlay-reconciler";
29
39
  import {
30
40
  configureAppOwnedMemoryBudget,
31
41
  enforceAppOwnedMemoryBudget,
@@ -36,18 +46,26 @@ import {
36
46
  registerDefaultAppOwnedMemoryStores,
37
47
  registerDefaultAppOwnedObservedBuffers,
38
48
  } from "../lib/app-owned-memory-stores";
39
- import { setStorageCleanupPolicyLiveSink } from "../storage/policy";
40
- import { setStorageCleanupPolicyJobLiveApply } from "../storage/policy-job";
41
- import { scheduleStorageCleanupStartupRun, startStorageCleanupScheduler } from "../storage/policy-scheduler";
49
+ import { acquireServerBackgroundLifecycle } from "./background-lifecycle";
50
+ import { activateLab, labActivationRequired } from "../lib/lab-activation";
42
51
  import { runOpenAiTierStartupMigration } from "../providers/openai-tier-startup";
43
52
  import { runAlibabaRegionStartupMigration } from "../providers/alibaba-region-startup";
44
- import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
53
+ import { runModelRenameStartupMigration } from "../providers/model-rename-startup";
54
+ import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
45
55
  import { providerCodexAccountMode } from "../providers/registry";
46
56
  import type { StorageCleanupPolicy } from "../types";
57
+ import { MAX_DECOMPRESSED_BODY_BYTES } from "./request-decompress";
47
58
  import {
48
59
  CodexAccountCooldownError,
49
60
  cooldownErrorMessage,
50
61
  } from "../codex/auth-context";
62
+ import { codexAccountNamespaceForModel } from "../codex/account-namespace-match";
63
+ import { codexAccountNamespaceEntries, isMainCodexAccountTarget } from "../codex/account-namespaces";
64
+ import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
65
+ import {
66
+ availableAccountGatedNativeModels,
67
+ resolveCodexModelEntitlements,
68
+ } from "../codex/model-entitlements";
51
69
  export {
52
70
  clearThreadAccountMap,
53
71
  formatCodexProviderForLog,
@@ -56,7 +74,7 @@ export {
56
74
  import { formatCodexProviderForLog } from "../codex/routing";
57
75
  import { CatalogGatherBusyError } from "../codex/catalog/provider-fetch";
58
76
  import { registerCodexWebSocket, tryReserveCodexWebSocket, unregisterCodexWebSocket, updateCodexWebSocketAuthContext } from "../codex/websocket-registry";
59
- import { resolveGuiFilePath, rootFallbackPayload, serveGuiFile } from "./gui-static";
77
+ import { resolveGuiFilePath, rootFallbackPayload, serveGuiFile, serveSessionBootstrap } from "./gui-static";
60
78
  export { resolveGuiFilePath, rootFallbackPayload } from "./gui-static";
61
79
  export { resolveAdapter } from "./adapter-resolve";
62
80
  import { formatErrorResponse, type ResponsesTerminalStatus } from "../bridge";
@@ -65,6 +83,7 @@ import {
65
83
  getActiveTurnCount,
66
84
  isDraining,
67
85
  registerTurn,
86
+ runListenerShutdown,
68
87
  setServerRef,
69
88
  trackStreamLifetime,
70
89
  tryAdmitTurn,
@@ -85,13 +104,13 @@ import {
85
104
  addFinalRequestLog,
86
105
  hydrateRequestLogsFromDisk,
87
106
  httpStatusForRequestLogTerminal,
88
- httpStatusForTerminalStatus,
89
107
  inspectResponseLogSsePayload,
90
108
  nextRequestLogId,
91
109
  recordFirstOutput,
92
110
  type RequestLogContext,
93
111
  type RequestLogEntry,
94
112
  } from "./request-log";
113
+ import { sessionLaneIdFromRequest } from "./request-log-conversation";
95
114
  export {
96
115
  addFinalRequestLog,
97
116
  filterRequestLogs,
@@ -132,6 +151,8 @@ import {
132
151
  admissionFields,
133
152
  resolveApiAuth,
134
153
  resolveResponsesApiAuth,
154
+ requestPolicyView,
155
+ type RequestPolicyView,
135
156
  safeConfigDTO,
136
157
  setCorsOrigin,
137
158
  withCors,
@@ -153,24 +174,165 @@ import { handleChatCompletions } from "./chat-completions";
153
174
  import { anthropicErrorResponse } from "../claude/outbound";
154
175
  import { buildDesktop3pRegistry } from "../claude/desktop-3p";
155
176
  import { runClaudeAuthModeMigration } from "../claude/auth-mode-migration";
177
+ import {
178
+ bindNativeMainStartupLifecycle,
179
+ blockNativeMainStartupForUnownedServiceHome,
180
+ prepareNativeMainStartupLifecycle,
181
+ releaseNativeMainStartupLifecycle,
182
+ type NativeMainStartupGateDeps,
183
+ type NativeMainStartupLifecycle,
184
+ } from "../codex/native-profile-startup";
156
185
  import { handleImages } from "./images";
157
186
  import { handleLive, logLiveSidebandFrame, parseLiveSidebandTarget, resolveLiveSidebandUpgrade } from "./live";
158
187
  import { handleSearch } from "./search";
159
- import { fetchAllModels, handleManagementAPI, VERSION } from "./management-api";
160
- import { initializeManagementAuthState, issueGuiSession, requireManagementAuth } from "./management-auth";
188
+ import { fetchAllModels, handleManagementAPI, VERSION, type ManagementApiDeps } from "./management-api";
189
+ import {
190
+ initializeManagementAuthState,
191
+ issueGuiSession,
192
+ managementPrincipal,
193
+ requireManagementAuth,
194
+ type ManagementAuthState,
195
+ } from "./management-auth";
196
+ import {
197
+ LOCAL_ATTESTATION_CHALLENGE_HEADER,
198
+ LOCAL_ATTESTATION_PROOF_HEADER,
199
+ createLocalAttestationProof,
200
+ createLocalAttestationSecret,
201
+ } from "../lib/local-management-attestation";
202
+ import { SYSTEM_RESTART_CAPABILITY_VERSION } from "../lib/system-restart-contract";
203
+ import { LOCAL_PROVIDER_RELOAD_CAPABILITY_VERSION } from "../lib/local-provider-reload-contract";
204
+ import { createReadinessGate, type ReadinessGate } from "./readiness";
205
+ import {
206
+ createRuntimePackageTreeIntegrityGuard,
207
+ type PackageTreeIntegrityGuard,
208
+ } from "../lib/package-tree-integrity";
209
+ import { detectInstall } from "../update/index";
161
210
 
162
- const MAX_WS_FRAME_BYTES = 50 * 1024 * 1024;
211
+ export const MAX_WS_FRAME_BYTES = 50 * 1024 * 1024;
163
212
  const WEBSOCKET_IDLE_TIMEOUT_SECONDS = 0;
164
213
  const LIVE_SIDEBAND_PENDING_MAX = 32;
214
+ const LIVE_SIDEBAND_PENDING_BYTES_MAX = 1024 * 1024;
215
+ const LIVE_SIDEBAND_CLOSE_FALLBACK_MS = 1_000;
165
216
 
166
- function closeLiveSideband(ws: ServerWebSocket<WsData>, code = 1000, reason = ""): void {
167
- try {
168
- ws.data.liveUpstream?.close(code, reason);
169
- } catch {
170
- /* upstream already gone */
217
+ export function exceedsLiveSidebandFrameByteLimit(frameBytes: number): boolean {
218
+ return frameBytes > MAX_WS_FRAME_BYTES;
219
+ }
220
+
221
+ export function exceedsLiveSidebandPendingByteLimit(pendingBytes: number, incomingBytes: number): boolean {
222
+ return incomingBytes > LIVE_SIDEBAND_PENDING_BYTES_MAX - pendingBytes;
223
+ }
224
+
225
+ function webSocketFrameBytes(frame: string | ArrayBuffer | ArrayBufferView | Blob | Buffer): number {
226
+ if (typeof frame === "string") return Buffer.byteLength(frame);
227
+ if (frame instanceof ArrayBuffer || ArrayBuffer.isView(frame)) return frame.byteLength;
228
+ return frame.size;
229
+ }
230
+
231
+ export type LiveSidebandPendingEnqueueResult = "queued" | "too-many-frames" | "too-many-bytes";
232
+
233
+ export function enqueueLiveSidebandPendingFrame(
234
+ data: Pick<WsData, "livePending" | "livePendingBytes">,
235
+ frame: string | Buffer,
236
+ frameBytes = webSocketFrameBytes(frame),
237
+ ): LiveSidebandPendingEnqueueResult {
238
+ const pending = data.livePending ?? (data.livePending = []);
239
+ if (pending.length >= LIVE_SIDEBAND_PENDING_MAX) return "too-many-frames";
240
+ const pendingBytes = data.livePendingBytes ?? 0;
241
+ if (exceedsLiveSidebandPendingByteLimit(pendingBytes, frameBytes)) return "too-many-bytes";
242
+ pending.push(frame);
243
+ data.livePendingBytes = pendingBytes + frameBytes;
244
+ return "queued";
245
+ }
246
+
247
+ type LiveSidebandWebSocketFactory = (
248
+ url: string,
249
+ headers: Record<string, string>,
250
+ ) => WebSocket;
251
+
252
+ function releaseLiveSidebandAdmission(ws: ServerWebSocket<WsData>): void {
253
+ ws.data.liveTurnAdmissionLease?.release();
254
+ ws.data.liveTurnAdmissionLease = undefined;
255
+ }
256
+
257
+ /**
258
+ * Send one live-sideband frame to the upstream socket.
259
+ *
260
+ * Bun's `WebSocket.send` accepts `string | Blob | BufferSource`, but the DOM-lib
261
+ * `Buffer` can be backed by a `SharedArrayBuffer`, which `BufferSource` rejects.
262
+ * `Uint8Array.from` copies into a fresh `ArrayBuffer`-backed view, so a frame
263
+ * arriving from `node:buffer` still round-trips byte-for-byte.
264
+ */
265
+ function sendUpstreamFrame(upstream: WebSocket, frame: string | Buffer): void {
266
+ if (typeof frame === "string") {
267
+ upstream.send(frame);
268
+ return;
269
+ }
270
+ upstream.send(Uint8Array.from(frame));
271
+ }
272
+
273
+ function finalizeLiveSideband(ws: ServerWebSocket<WsData>, upstream?: WebSocket): void {
274
+ if (upstream && ws.data.liveUpstream !== upstream) return;
275
+ if (ws.data.liveCloseFallback !== undefined) {
276
+ clearTimeout(ws.data.liveCloseFallback);
277
+ ws.data.liveCloseFallback = undefined;
171
278
  }
172
279
  ws.data.liveUpstream = undefined;
173
280
  ws.data.livePending = undefined;
281
+ ws.data.livePendingBytes = undefined;
282
+ ws.data.cancel = undefined;
283
+ releaseLiveSidebandAdmission(ws);
284
+ }
285
+
286
+ function armLiveSidebandCloseFallback(ws: ServerWebSocket<WsData>, upstream: WebSocket): void {
287
+ if (ws.data.liveCloseFallback !== undefined) return;
288
+ ws.data.liveCloseFallback = setTimeout(() => {
289
+ ws.data.liveCloseFallback = undefined;
290
+ if (ws.data.liveUpstream !== upstream) return;
291
+ if (upstream.readyState === WebSocket.CLOSED) {
292
+ finalizeLiveSideband(ws, upstream);
293
+ return;
294
+ }
295
+ // A close frame was already sent below. Retry once, but never surrender
296
+ // native-main ownership while the authenticated transport remains live.
297
+ try {
298
+ upstream.close(1000, "upstream close timeout");
299
+ } catch {
300
+ /* upstream is already unusable */
301
+ }
302
+ // Some implementations transition synchronously without delivering the
303
+ // close event. That is still an observed CLOSED transport and is safe to
304
+ // finalize. CONNECTING/CLOSING peers keep the lease so profile switching
305
+ // fails at its own bounded drain deadline instead of racing live traffic.
306
+ // The earlier CLOSED check narrowed `readyState` to 0|1|2 in the type
307
+ // system, but the socket can still transition to CLOSED (3) before this
308
+ // fallback fires; the cast keeps the runtime-identical check.
309
+ if ((upstream.readyState as number) === 3) finalizeLiveSideband(ws, upstream);
310
+ }, LIVE_SIDEBAND_CLOSE_FALLBACK_MS);
311
+ }
312
+
313
+ function closeLiveSideband(ws: ServerWebSocket<WsData>, code = 1000, reason = ""): void {
314
+ if (ws.data.liveClosing) return;
315
+ ws.data.liveClosing = true;
316
+ ws.data.livePending = undefined;
317
+ ws.data.livePendingBytes = undefined;
318
+ ws.data.cancel = undefined;
319
+ const upstream = ws.data.liveUpstream;
320
+ // Bun's `WebSocket` type narrows `readyState` to 0|1|2 even though the DOM
321
+ // constant CLOSED is 3; the numeric literal is the runtime-identical check.
322
+ if (!upstream || upstream.readyState === 3) {
323
+ finalizeLiveSideband(ws, upstream);
324
+ } else {
325
+ // The sideband holds a native-main admission lease. Do not release it just
326
+ // because the downstream left: its authenticated upstream remains live
327
+ // until the close event arrives or the transport is observed CLOSED. The
328
+ // bounded fallback only retries close; it does not release ownership.
329
+ armLiveSidebandCloseFallback(ws, upstream);
330
+ try {
331
+ upstream.close(code, reason);
332
+ } catch {
333
+ /* the fallback retries close without releasing ownership */
334
+ }
335
+ }
174
336
  try {
175
337
  if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
176
338
  ws.close(code, reason);
@@ -180,7 +342,12 @@ function closeLiveSideband(ws: ServerWebSocket<WsData>, code = 1000, reason = ""
180
342
  }
181
343
  }
182
344
 
183
- function attachLiveSidebandUpstream(ws: ServerWebSocket<WsData>): void {
345
+ function attachLiveSidebandUpstream(
346
+ ws: ServerWebSocket<WsData>,
347
+ createWebSocket: LiveSidebandWebSocketFactory = (url, headers) => (
348
+ new WebSocket(url, { headers } as unknown as string[])
349
+ ),
350
+ ): void {
184
351
  const url = ws.data.liveUpstreamUrl;
185
352
  if (!url) {
186
353
  closeLiveSideband(ws, 1011, "missing upstream");
@@ -189,27 +356,24 @@ function attachLiveSidebandUpstream(ws: ServerWebSocket<WsData>): void {
189
356
  let upstream: WebSocket;
190
357
  try {
191
358
  // Bun accepts per-handshake headers; the DOM lib types only list protocol arrays.
192
- upstream = new WebSocket(url, { headers: ws.data.liveUpstreamHeaders ?? {} } as unknown as string[]);
359
+ upstream = createWebSocket(url, ws.data.liveUpstreamHeaders ?? {});
193
360
  } catch {
194
361
  closeLiveSideband(ws, 1011, "upstream connect failed");
195
362
  return;
196
363
  }
197
364
  ws.data.liveUpstream = upstream;
198
- ws.data.cancel = () => {
199
- try {
200
- upstream.close(1000, "client closed");
201
- } catch {
202
- /* ignore */
203
- }
204
- };
365
+ ws.data.liveClosing = false;
366
+ ws.data.cancel = () => closeLiveSideband(ws, 1000, "client closed");
205
367
 
206
368
  upstream.addEventListener("open", () => {
369
+ if (ws.data.liveUpstream !== upstream || ws.data.liveClosing) return;
207
370
  ws.data.liveOpened = true;
208
371
  const pending = ws.data.livePending ?? [];
209
372
  ws.data.livePending = undefined;
373
+ ws.data.livePendingBytes = undefined;
210
374
  for (const frame of pending) {
211
375
  try {
212
- upstream.send(frame);
376
+ sendUpstreamFrame(upstream, frame);
213
377
  } catch {
214
378
  closeLiveSideband(ws, 1011, "upstream send failed");
215
379
  return;
@@ -217,7 +381,12 @@ function attachLiveSidebandUpstream(ws: ServerWebSocket<WsData>): void {
217
381
  }
218
382
  });
219
383
  upstream.addEventListener("message", (event) => {
384
+ if (ws.data.liveUpstream !== upstream || ws.data.liveClosing) return;
220
385
  try {
386
+ if (exceedsLiveSidebandFrameByteLimit(webSocketFrameBytes(event.data))) {
387
+ closeLiveSideband(ws, 1009, "message too large");
388
+ return;
389
+ }
221
390
  logLiveSidebandFrame("u2c", event.data);
222
391
  if (typeof event.data === "string") ws.send(event.data);
223
392
  else if (event.data instanceof ArrayBuffer) ws.send(event.data);
@@ -229,14 +398,17 @@ function attachLiveSidebandUpstream(ws: ServerWebSocket<WsData>): void {
229
398
  }
230
399
  });
231
400
  upstream.addEventListener("close", (event) => {
401
+ if (ws.data.liveUpstream !== upstream) return;
402
+ ws.data.liveClosing = true;
403
+ finalizeLiveSideband(ws, upstream);
232
404
  try {
233
405
  ws.close(event.code || 1000, event.reason || "");
234
406
  } catch {
235
407
  /* ignore */
236
408
  }
237
- ws.data.liveUpstream = undefined;
238
409
  });
239
410
  upstream.addEventListener("error", () => {
411
+ if (ws.data.liveUpstream !== upstream) return;
240
412
  closeLiveSideband(ws, 1011, "upstream error");
241
413
  });
242
414
  }
@@ -250,18 +422,17 @@ function attachLiveSidebandUpstream(ws: ServerWebSocket<WsData>): void {
250
422
  // if (isEventStream && upstreamResponse.body) {
251
423
  // const repairConfig = route.provider.responsesItemIdRepair;
252
424
  // const needsClientRewrite = imageGenCallAliases.size > 0
253
- // #314 gated shape: win32 no-rewrite traffic follows runtime/config policy; darwin no-rewrite
254
- // traffic requires explicit config-eager opt-in (`auto` always stays tee on darwin). Default OFF
255
- // on the bundled known-bad runtime; policy lives in 260731_macos_rss_retention phase 100.
425
+ // #314 gated shape: win32 always uses the terminal-aware eager relay so a keep-alive
426
+ // upstream cannot hold Codex open after response.completed; darwin no-rewrite traffic
427
+ // requires explicit config-eager opt-in (`auto` always stays tee on darwin).
256
428
  // selectEagerPath(process.platform, needsClientRewrite, config.streamMode ?? "auto")
429
+ // Codex upstream WS runtime gating and the forced bounded single-reader branch
430
+ // are owned by responses/ws-upstream.ts and responses/core.ts respectively.
257
431
  // relaySseEagerBounded(upstreamResponse.body, turnAc,
258
432
  // new Response(eagerBody,
259
433
  // Default shape (tee + background inspection):
260
434
  // upstreamResponse.body.tee()
261
435
  // const repairedBody = hasResponsesItemIdRepair(repairConfig)
262
- // process.platform === "win32"
263
- // && !needsClientRewrite
264
- // ? nativeBody
265
436
  // relaySseWithFailedTail(repairedBody, upstream)
266
437
  // new Response(clientBody
267
438
  // markNativePassthroughSseResponse
@@ -271,12 +442,92 @@ function attachLiveSidebandUpstream(ws: ServerWebSocket<WsData>): void {
271
442
  // trackSseForRequestLog(
272
443
  // export function relaySseWithHeartbeat
273
444
 
274
- export function startServer(port?: number) {
275
- const config = runAlibabaRegionStartupMigration(runOpenAiTierStartupMigration(loadConfig()));
445
+ export interface StartServerDeps {
446
+ /** Test-only seam; production always initializes its own management credential state. */
447
+ managementAuthState?: ManagementAuthState;
448
+ /** Test-only route dependencies, forwarded only after management admission succeeds. */
449
+ managementApi?: ManagementApiDeps;
450
+ /** Test-only native-main recovery dependencies; production constructs the normal manager. */
451
+ nativeMainStartup?: NativeMainStartupGateDeps;
452
+ /** Test-only ownership evidence; production inspects the installed service state. */
453
+ inspectNativeCodexOwnership?: typeof inspectNativeCodexOwnership;
454
+ /** Test-only service-home resolver; production resolves the current homes directly. */
455
+ resolveServiceHomes?: typeof currentServiceHomes;
456
+ /** Test-only seam for an upstream that cannot complete its WebSocket close handshake. */
457
+ liveSidebandWebSocketFactory?: LiveSidebandWebSocketFactory;
458
+ /** Test-only seam; production derives a fresh local-attestation secret per process. */
459
+ localAttestationSecret?: string;
460
+ /** Optional readiness gate; a fresh pending gate is created when omitted. */
461
+ readinessGate?: ReadinessGate;
462
+ /** Test-only package-tree observation; production captures package.json identity at boot. */
463
+ packageTreeIntegrity?: PackageTreeIntegrityGuard;
464
+ }
465
+
466
+ function inspectStartupOwnership(
467
+ deps: StartServerDeps,
468
+ currentHomes: ReturnType<typeof currentServiceHomes> | null,
469
+ statePaths: readonly string[] | null,
470
+ ): OwnershipInspection {
471
+ try {
472
+ if (currentHomes === null || statePaths === null) {
473
+ return {
474
+ ownership: "unknown",
475
+ reason: "startup service-home resolution failed",
476
+ };
477
+ }
478
+ if (deps.inspectNativeCodexOwnership) {
479
+ return deps.inspectNativeCodexOwnership({ currentHomes, statePaths });
480
+ }
481
+ return inspectNativeCodexOwnership({ currentHomes, statePaths });
482
+ } catch {
483
+ return {
484
+ ownership: "unknown",
485
+ reason: "service-home ownership inspection failed",
486
+ };
487
+ }
488
+ }
489
+
490
+ /*
491
+ * #1046. `startServer` rewrites the Codex models cache during boot, and an
492
+ * app-server that started earlier keeps its own in-memory model list. The stale
493
+ * warning is not emitted here: `handleStart` runs a catalog sync moments later,
494
+ * so warning now would read an mtime that write is about to move, and both sites
495
+ * calling the helper independently would warn twice. This records the fact; the
496
+ * CLI start path owns the single decision.
497
+ *
498
+ * A caller that starts a server without `handleStart` (tests, embedded use)
499
+ * deliberately gets no warning — lifecycle diagnostics belong to whoever owns
500
+ * the lifecycle.
501
+ */
502
+ let startupCacheInvalidationWrote = false;
503
+
504
+ /** #1046: did this process's startup cache invalidation actually write? */
505
+ export function consumeStartupCacheInvalidationWrite(): boolean {
506
+ const wrote = startupCacheInvalidationWrote;
507
+ startupCacheInvalidationWrote = false;
508
+ return wrote;
509
+ }
510
+
511
+ export function warnAgentTaskRecoveryStartup(config: {
512
+ agentTaskRecovery?: { enabled?: boolean };
513
+ }): void {
514
+ if (config.agentTaskRecovery?.enabled !== true) return;
515
+ console.warn("⚠️ Experimental encrypted V2 task recovery is enabled.");
516
+ console.warn(" A scoped cache miss may send an additional authenticated request to ChatGPT and may consume quota or add latency; concurrent misses can share one request.");
517
+ console.warn(" Recovered plaintext assignment data is retained only in a bounded, process-local in-memory cache; exact fidelity is not guaranteed and the path depends on undocumented backend behavior.");
518
+ }
519
+
520
+ export function startServer(port?: number, deps: StartServerDeps = {}): Server<WsData> {
521
+ const localAttestationSecret = deps.localAttestationSecret ?? createLocalAttestationSecret();
522
+ const config = runModelRenameStartupMigration(runAlibabaRegionStartupMigration(runOpenAiTierStartupMigration(loadConfig())));
523
+ warnAgentTaskRecoveryStartup(config);
276
524
  setLiveStateStoreConfig(config);
277
525
  applyProxyEnv(config);
278
526
  assertServerAuthConfig(config);
279
- const managementAuth = initializeManagementAuthState(config);
527
+ const managementAuth = deps.managementAuthState ?? initializeManagementAuthState(config);
528
+ let userCostOverlayReconciler: { stop(): void } | null = null;
529
+ // Arm synchronously before listen. A pending journal therefore makes __main__ unusable
530
+ // before any request can resolve its physical credential, while health/management/Pool stay live.
280
531
  // Refresh OAuth provider presets (models/noReasoningModels) from the registry so a proxy update
281
532
  // adding/dropping models reaches existing configs on start — not just fresh installs.
282
533
  reconcileOAuthProviders(config);
@@ -310,7 +561,40 @@ export function startServer(port?: number) {
310
561
  if (migrated) saveConfig(config);
311
562
  }
312
563
  }
313
- invalidateCodexModelsCache();
564
+ // Resolve unattended service-home authority before any Codex lock, cache, owner,
565
+ // journal, or credential path. Both positive foreign evidence and an unprovable
566
+ // ownership state are non-authority.
567
+ startupCacheInvalidationWrote = false;
568
+ const resolveServiceHomes = deps.resolveServiceHomes ?? currentServiceHomes;
569
+ let startupOwnershipHomes: ReturnType<typeof currentServiceHomes> | null = null;
570
+ let startupOwnershipStatePaths: readonly string[] | null = null;
571
+ try {
572
+ const homes = resolveServiceHomes();
573
+ const statePaths = serviceStatePathsForOpenCodexHome(homes.opencodexHome);
574
+ startupOwnershipHomes = homes;
575
+ startupOwnershipStatePaths = statePaths;
576
+ } catch { /* inspection below stays unknown */ }
577
+ const startupCacheOwnership = inspectStartupOwnership(
578
+ deps,
579
+ startupOwnershipHomes,
580
+ startupOwnershipStatePaths,
581
+ );
582
+ // Startup cache invalidation is best-effort and must never block the server from
583
+ // serving. It now takes K so it cannot race a convergence commit. Use the home
584
+ // paired with the ownership inspection; re-reading ambient CODEX_HOME here could
585
+ // invalidate a different installation after an environment or mount change.
586
+ if (startupCacheOwnership.ownership === "owned" && startupOwnershipHomes !== null) {
587
+ try {
588
+ const startupCodexHome = startupOwnershipHomes.codexHome;
589
+ // #1046: record whether this actually rewrote the cache. `handleStart` ORs this
590
+ // with the later startup sync and warns ONCE about stale app-servers; warning
591
+ // here instead would read a catalog mtime the sync is about to move.
592
+ const outcome = withCatalogWriteSerialization(startupCodexHome, permit =>
593
+ invalidateCodexModelsCacheWithPermit(permit, startupCodexHome));
594
+ // A refused permit is not a write; only a completed run that returned true is.
595
+ startupCacheInvalidationWrote = outcome.kind === "completed" && outcome.value === true;
596
+ } catch { /* no readable Codex home: nothing to invalidate */ }
597
+ }
314
598
  // Arm the `claudeCode` hand-edit guard (devlog 260726_claude_auth_auto/040 H1) BEFORE
315
599
  // the server can serve a request, and AFTER the startup migrations above — those run
316
600
  // against a config nobody else holds and are the documented exception to the save
@@ -321,15 +605,12 @@ export function startServer(port?: number) {
321
605
  // usage.jsonl already persists every request; rehydrate the in-memory Logs ring so
322
606
  // /api/logs (and the GUI) survive `ocx stop` / `ocx start` process restarts.
323
607
  hydrateRequestLogsFromDisk();
324
- // #314: warn-only RSS observability (unref'd, idempotent — safe under repeated
325
- // startServer(0) in tests). Snapshot surfaces via GET /api/system/memory.
326
- startMemoryWatchdog();
327
608
  registerDefaultAppOwnedMemoryStores();
328
609
  registerDefaultAppOwnedObservedBuffers();
329
610
  registerAppOwnedMemorySweepFallback();
330
611
  configureAppOwnedMemoryBudget(resolveAppOwnedMemoryBudgetBytes(config.appOwnedMemoryBudgetMb));
331
612
  enforceAppOwnedMemoryBudget();
332
- startStateStoreSweeper();
613
+ registerCodexCooldownRecoveryProbeWorker(config);
333
614
  // Issue #42 Phase 3: opt-in archived auto-cleanup (default OFF). Unref'd hourly
334
615
  // tick for daily/weekly; startup evaluation is fire-and-forget after listen.
335
616
  // Heavy work runs in a Worker via the single-flight job controller.
@@ -337,9 +618,6 @@ export function startServer(port?: number) {
337
618
  const applyPolicy = (policy: StorageCleanupPolicy) => {
338
619
  config.storageCleanupPolicy = policy;
339
620
  };
340
- setStorageCleanupPolicyLiveSink(applyPolicy);
341
- setStorageCleanupPolicyJobLiveApply(applyPolicy);
342
- startStorageCleanupScheduler();
343
621
 
344
622
  const listenPort = port ?? config.port ?? 10100;
345
623
  setCorsOrigin(listenPort);
@@ -351,30 +629,98 @@ export function startServer(port?: number) {
351
629
  const configuredHost = config.hostname?.trim();
352
630
  const bindHost = !configuredHost || /^localhost$/i.test(configuredHost) ? "127.0.0.1" : configuredHost;
353
631
 
632
+ // Unauthenticated loopback listener (#1102). Off unless explicitly enabled.
633
+ const loopbackListener = config.unauthenticatedLoopbackListener;
634
+ const loopbackListenerPort = loopbackListener?.enabled ? loopbackListener.port : null;
635
+
636
+ /**
637
+ * Which listener a request arrived on, expressed as the only thing that differs: the bind
638
+ * address the auth and CORS decisions should see.
639
+ *
640
+ * The public listener passes the shared config through untouched, so its behaviour is
641
+ * byte-identical to before. The loopback listener substitutes 127.0.0.1, which is what makes
642
+ * `isApiAuthRequired` return false for it — the same code path a plain loopback bind has
643
+ * always taken, including the Host-header check inside `isAllowedRequestOrigin`.
644
+ *
645
+ * Built per request rather than once per listener so a management-API config change is
646
+ * picked up immediately instead of being frozen at listen time.
647
+ */
648
+ const publicPolicy = (): RequestPolicyView => config;
649
+ const loopbackPolicy = (): RequestPolicyView => requestPolicyView(config, "127.0.0.1");
650
+ void publicPolicy;
651
+
652
+ /**
653
+ * Routes the unauthenticated loopback listener will serve. Everything else 404s.
654
+ *
655
+ * This is an allowlist rather than a filter applied to the public handler, because a filter
656
+ * inverts the failure mode: a route added later would be reachable here by default. The four
657
+ * entries are exactly what a directly-spawned `codex app-server` needs.
658
+ *
659
+ * `GET /v1/models` is on the list for a reason that is easy to miss. When catalog
660
+ * materialization fails or finds no source, `syncCodex` warns and injects with
661
+ * `catalogPath: null`; Codex then builds an ONLINE model manager and `model/list` refreshes
662
+ * through `GET {base_url}/models`. Returning 404 there would leave the picker on its bundled
663
+ * fallback — fixing the direct-spawn host while breaking its model list.
664
+ */
665
+ function loopbackRouteAllowed(url: URL, req: Request): boolean {
666
+ const path = url.pathname;
667
+ if (path === "/v1/responses") {
668
+ return req.method === "POST" || req.headers.get("upgrade")?.toLowerCase() === "websocket";
669
+ }
670
+ if (path === "/v1/responses/compact") return req.method === "POST";
671
+ if (path === "/v1/models") return req.method === "GET";
672
+ // Standalone realtime voice sessions (codex-rs thread/realtime/start, WebSocket
673
+ // transport) — a directly-spawned `codex app-server` needs these for desktop
674
+ // voice the same way it needs /v1/responses. WebSocket upgrades only; plain
675
+ // HTTP on these paths stays rejected.
676
+ if (path === "/v1/realtime" || path === "/v1/live") {
677
+ return req.headers.get("upgrade")?.toLowerCase() === "websocket";
678
+ }
679
+ return false;
680
+ }
681
+
354
682
  // Codex treats empty / non-JSON 503 bodies as "Unknown error" (#452). Keep Retry-After and
355
683
  // the server_is_overloaded code so clients can back off, but always return a JSON envelope.
356
- function drainingResponse(req: Request): Response {
684
+ // These two run BEFORE the auth/origin checks, so they need the receiving listener's policy
685
+ // explicitly (#1102). Reaching for the shared `config` here would attach public-policy CORS
686
+ // headers to a 503 on the loopback listener — no model runs and no credential is spent, but
687
+ // it is the one error path that would answer a rebinding origin with its own origin echoed
688
+ // back.
689
+ function drainingResponse(req: Request, policy: RequestPolicyView): Response {
357
690
  const response = formatErrorResponse(503, "server_error", "Service shutting down");
358
691
  const headers = new Headers(response.headers);
359
- for (const [name, value] of Object.entries(corsHeaders(req, config))) {
692
+ for (const [name, value] of Object.entries(corsHeaders(req, policy))) {
360
693
  headers.set(name, value);
361
694
  }
362
695
  headers.set("Retry-After", "5");
363
696
  return new Response(response.body, { status: 503, headers });
364
697
  }
365
698
 
366
- function serverBusyResponse(req: Request, resource: string): Response {
699
+ function serverBusyResponse(req: Request, resource: string, policy: RequestPolicyView): Response {
367
700
  return withCors(new Response(JSON.stringify({
368
701
  error: { type: "server_error", code: "server_busy", message: `${resource} capacity reached` },
369
702
  }), {
370
703
  status: 503,
371
704
  headers: { "Content-Type": "application/json", "Retry-After": "1" },
372
- }), req, config);
705
+ }), req, policy);
373
706
  }
374
707
 
375
- async function runAdmittedHttpTurn(req: Request, work: (lease: ActiveTurnLease) => Promise<Response>): Promise<Response> {
376
- const lease = tryAdmitTurn();
377
- if (!lease) return serverBusyResponse(req, "active turns");
708
+ function packageTreeChangedResponse(req: Request, policy: RequestPolicyView, message: string): Response {
709
+ return withCors(new Response(JSON.stringify({
710
+ error: { type: "server_error", code: "package_tree_changed", message },
711
+ }), {
712
+ status: 503,
713
+ headers: { "Content-Type": "application/json" },
714
+ }), req, policy);
715
+ }
716
+
717
+ async function runAdmittedHttpTurn(
718
+ req: Request,
719
+ policy: RequestPolicyView,
720
+ work: (lease: ActiveTurnLease) => Promise<Response>,
721
+ ): Promise<Response> {
722
+ const lease = tryAdmitTurn(sessionLaneIdFromRequest(req.headers));
723
+ if (!lease) return serverBusyResponse(req, "active turns", policy);
378
724
  let response: Response;
379
725
  try {
380
726
  response = await work(lease);
@@ -388,25 +734,180 @@ export function startServer(port?: number) {
388
734
  return response;
389
735
  }
390
736
 
391
- const server: Server<WsData> = Bun.serve<WsData>({
392
- port: listenPort,
393
- hostname: bindHost,
394
- idleTimeout: 255,
395
- async fetch(req, requestServer): Promise<Response> {
737
+ // Readiness gate: one PRIVATE controller per startServer invocation, captured
738
+ // by this listener's closure. Starting/failing a second server in the same
739
+ // process can never reset or mutate this gate. handleStart creates the gate,
740
+ // passes it in, and transitions it after the post-startup sync settles. When
741
+ // no gate is supplied (tests, ad-hoc starts) a fresh pending gate is created.
742
+ const readinessGate = deps.readinessGate ?? createReadinessGate();
743
+ const packageTreeIntegrity = deps.packageTreeIntegrity
744
+ ?? createRuntimePackageTreeIntegrityGuard(detectInstall());
745
+ // Actual bound port, filled in after Bun.serve binds so /readyz reports the
746
+ // real ephemeral port for startServer(0). /healthz keeps its existing port
747
+ // field (the requested listenPort) byte-for-byte.
748
+ let boundPort: number | null = null;
749
+
750
+ // Native-main startup ownership creates several SQLite coordination files in
751
+ // CODEX_HOME. When the user has disabled the Codex integration, starting the
752
+ // proxy must not manufacture those Codex artifacts merely to serve other
753
+ // clients; no Codex request can use this lifecycle in that state.
754
+ // Re-probe here instead of trusting the earlier cache decision: startup work
755
+ // between the two sites must not widen the service-install race.
756
+ const nativeOwnership = inspectStartupOwnership(deps, startupOwnershipHomes, startupOwnershipStatePaths);
757
+ const preparedNativeMainLifecycle = nativeOwnership.ownership !== "foreign"
758
+ && startupOwnershipHomes !== null
759
+ ? prepareNativeMainStartupLifecycle(
760
+ deps.nativeMainStartup,
761
+ { codexHome: startupOwnershipHomes.codexHome, configDir: startupOwnershipHomes.opencodexHome },
762
+ )
763
+ : null;
764
+ let retryOwnershipHomes = startupOwnershipHomes;
765
+ let retryOwnershipStatePaths = startupOwnershipStatePaths;
766
+ let retryPreparedNativeMainLifecycle = preparedNativeMainLifecycle;
767
+ const reprobeNativeOwnership = (): NativeCodexOwnership => {
768
+ // If startup could not resolve the homes at all, preserve a bounded retry
769
+ // without guessing an authority. The first successful resolution is pinned
770
+ // together with its service-state paths before ownership is inspected.
771
+ if (retryOwnershipHomes === null || retryOwnershipStatePaths === null) {
772
+ try {
773
+ const homes = resolveServiceHomes();
774
+ const statePaths = serviceStatePathsForOpenCodexHome(homes.opencodexHome);
775
+ retryOwnershipHomes = homes;
776
+ retryOwnershipStatePaths = statePaths;
777
+ } catch {
778
+ return "unknown";
779
+ }
780
+ }
781
+ const homes = retryOwnershipHomes;
782
+ const statePaths = retryOwnershipStatePaths;
783
+ const answer = inspectStartupOwnership(deps, homes, statePaths).ownership;
784
+ if (answer !== "owned") return answer;
785
+ retryPreparedNativeMainLifecycle ??= prepareNativeMainStartupLifecycle(
786
+ deps.nativeMainStartup,
787
+ { codexHome: homes.codexHome, configDir: homes.opencodexHome },
788
+ );
789
+ // An ownership verdict without a lifecycle bound to that same home is not
790
+ // enough to reopen native-main admission.
791
+ return retryPreparedNativeMainLifecycle ? "owned" : "unknown";
792
+ };
793
+ const ownershipRetryOptions = {
794
+ reprobe: reprobeNativeOwnership,
795
+ expectedHomeId: () => retryPreparedNativeMainLifecycle?.homeId ?? null,
796
+ startOwnedLifecycle: () => {
797
+ if (!retryPreparedNativeMainLifecycle) {
798
+ throw new Error("Native-main ownership became known before its startup lifecycle was prepared.");
799
+ }
800
+ return retryPreparedNativeMainLifecycle.start();
801
+ },
802
+ };
803
+ const nativeMainLifecycle: NativeMainStartupLifecycle = shouldSyncCodexOnStart(config)
804
+ ? nativeOwnership.ownership === "owned"
805
+ ? preparedNativeMainLifecycle
806
+ ? preparedNativeMainLifecycle.start()
807
+ : blockNativeMainStartupForUnownedServiceHome(
808
+ "ownership-unknown",
809
+ ownershipRetryOptions,
810
+ )
811
+ : nativeOwnership.ownership === "foreign"
812
+ ? blockNativeMainStartupForUnownedServiceHome("foreign-ownership")
813
+ : blockNativeMainStartupForUnownedServiceHome(
814
+ "ownership-unknown",
815
+ // #2108: an `unknown` verdict means the probe could not answer, not that this host
816
+ // is unownable. Hand the fence a way to re-ask so a host that becomes answerable
817
+ // after boot reopens on its own instead of needing `ocx restart`. A `foreign`
818
+ // verdict ignores this by design — that one is a fact, not a question.
819
+ ownershipRetryOptions,
820
+ )
821
+ : {
822
+ homeId: null,
823
+ settled: Promise.resolve({ status: "ready", homeId: null }),
824
+ release: async () => {},
825
+ };
826
+ let server: Server<WsData>;
827
+ let loopbackServer: Server<WsData> | null = null;
828
+ let backgroundLifecycle: ReturnType<typeof acquireServerBackgroundLifecycle> | null = null;
829
+ try {
830
+ backgroundLifecycle = acquireServerBackgroundLifecycle(applyPolicy);
831
+ // External `ocx config set` / direct config.json edits run in other
832
+ // processes; poll the file so Logs/Usage display prices follow them live.
833
+ // Started inside the guarded startup transaction so the catch below can
834
+ // release the owner-scoped lease on any listener failure.
835
+ userCostOverlayReconciler = startUserCostOverlayReconciler({ liveConfig: config });
836
+ const serveOptions = {
837
+ idleTimeout: 255,
838
+ maxRequestBodySize: MAX_DECOMPRESSED_BODY_BYTES,
839
+ async fetch(req: Request, requestServer: Server<WsData>): Promise<Response> {
840
+ // The unauthenticated loopback listener (#1102) serves a fixed allowlist and nothing
841
+ // else. Rejecting here, before any handler runs, is what keeps the surface from growing
842
+ // silently when a route is added below.
843
+ if (requestServer === loopbackServer && !loopbackRouteAllowed(new URL(req.url), req)) {
844
+ return withCors(
845
+ formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${new URL(req.url).pathname}`),
846
+ req,
847
+ loopbackPolicy(),
848
+ );
849
+ }
850
+ // Auth and CORS decisions below read `policy`, not `config`. For the public listener the
851
+ // two are the same object, so its behaviour is unchanged; for the loopback listener the
852
+ // view substitutes 127.0.0.1 as the bind address, which is what routes it through the
853
+ // same code path a plain loopback bind has always taken — Host-header check included.
854
+ // Routing, provider selection and response bodies keep using `config`.
855
+ const policy: RequestPolicyView = requestServer === loopbackServer ? loopbackPolicy() : config;
396
856
  const url = new URL(req.url);
397
857
  markActivity(`${req.method} ${url.pathname}`);
398
858
 
859
+ // Readiness is exact-GET on the literal /readyz path. Compare the DECODED
860
+ // pathname so an encoded variant like /readyz%2F (which decodes to
861
+ // /readyz/) cannot bypass the exact-path rejection and reach the GUI
862
+ // fallback (serveGuiFile decodes the pathname and would serve index.html
863
+ // with 200). Malformed percent-sequences fall back to the raw pathname,
864
+ // which still cannot match the exact literal below.
865
+ let readyzPath: string | undefined;
866
+ try {
867
+ const decoded = decodeURIComponent(url.pathname);
868
+ if (decoded === "/readyz" || decoded === "/readyz/") readyzPath = decoded;
869
+ } catch { /* malformed encoding — not a readiness path */ }
870
+
871
+ const packageTreeStatus = packageTreeIntegrity.status();
872
+ if (!packageTreeStatus.ok && (
873
+ url.pathname === "/healthz"
874
+ || readyzPath !== undefined
875
+ || url.pathname.startsWith("/v1/")
876
+ )) {
877
+ const message = "OpenCodex package files changed while this proxy was running; restart OpenCodex before retrying.";
878
+ const response = url.pathname === "/healthz" || readyzPath !== undefined
879
+ ? jsonResponse({
880
+ status: "restart_required",
881
+ service: "opencodex",
882
+ version: VERSION,
883
+ uptime: process.uptime(),
884
+ pid: process.pid,
885
+ port: boundPort ?? requestServer.port ?? listenPort,
886
+ error: { code: "package_tree_changed", message },
887
+ }, 503, req, policy)
888
+ : packageTreeChangedResponse(req, policy, message);
889
+ const headers = new Headers(response.headers);
890
+ headers.set("Retry-After", "5");
891
+ return new Response(response.body, { status: 503, headers });
892
+ }
893
+
399
894
  if (req.method === "OPTIONS") {
895
+ // /readyz is exact-GET only; OPTIONS (like POST and the trailing-slash
896
+ // path) must answer the deterministic JSON 404, never the generic 204
897
+ // preflight response that the SPA fallback would otherwise allow.
898
+ if (readyzPath !== undefined) {
899
+ return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, policy);
900
+ }
400
901
  const managementPreflight = url.pathname.startsWith("/api/");
401
902
  const allowed = managementPreflight
402
903
  ? isAllowedManagementOrigin(req, config)
403
- : isAllowedRequestOrigin(req, config);
904
+ : isAllowedRequestOrigin(req, policy);
404
905
  if (!allowed) {
405
906
  return new Response(null, { status: 403, headers: corsHeaders() });
406
907
  }
407
908
  return new Response(null, {
408
909
  status: 204,
409
- headers: managementPreflight ? managementCorsHeaders(req, config) : corsHeaders(req, config),
910
+ headers: managementPreflight ? managementCorsHeaders(req, config) : corsHeaders(req, policy),
410
911
  });
411
912
  }
412
913
 
@@ -414,14 +915,14 @@ export function startServer(port?: number) {
414
915
  // handshake-time only, so capture inbound headers and thread them into the pipeline.
415
916
  if (url.pathname === "/v1/responses" && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
416
917
  if (isDraining()) {
417
- return drainingResponse(req);
918
+ return drainingResponse(req, policy);
418
919
  }
419
- const admission = resolveResponsesApiAuth(req, config);
920
+ const admission = resolveResponsesApiAuth(req, policy);
420
921
  if (!admission) {
421
- return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
922
+ return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
422
923
  }
423
- if (!isAllowedRequestOrigin(req, config)) {
424
- return withCors(formatErrorResponse(403, "origin_rejected", "WebSocket upgrade blocked: non-local Origin"), req, config);
924
+ if (!isAllowedRequestOrigin(req, policy)) {
925
+ return withCors(formatErrorResponse(403, "origin_rejected", "WebSocket upgrade blocked: non-local Origin"), req, policy);
425
926
  }
426
927
  // WS transport gate: Codex's built-in `openai` provider hardcodes supports_websockets=true,
427
928
  // so under Design B it always tries the WS transport first. When the feature is off, reject
@@ -429,26 +930,96 @@ export function startServer(port?: number) {
429
930
  // session-scoped HTTP fallback (client.rs WebsocketStreamOutcome::FallbackToHttp) instead of
430
931
  // surfacing broken-pipe errors from sockets a "disabled" feature would otherwise accept.
431
932
  if (!websocketsEnabled(config)) {
432
- return withCors(formatErrorResponse(426, "upgrade_required", "Responses WebSocket transport is disabled; use HTTP"), req, config);
933
+ return withCors(formatErrorResponse(426, "upgrade_required", "Responses WebSocket transport is disabled; use HTTP"), req, policy);
433
934
  }
434
935
  const websocketLease = tryReserveCodexWebSocket();
435
- if (!websocketLease) return serverBusyResponse(req, "Codex WebSockets");
436
- if (server.upgrade(req, {
437
- data: buildResponsesWsData(selectForwardHeaders(req.headers), admission, websocketLease),
936
+ if (!websocketLease) return serverBusyResponse(req, "Codex WebSockets", policy);
937
+ // Upgrade on the server that RECEIVED this request, not the captured `server`
938
+ // binding. They are the same object for the public listener, but the
939
+ // unauthenticated loopback listener (#1102) is a second Bun.serve, and handing its
940
+ // request to the public server's upgrade would fail or cross sockets.
941
+ if (requestServer.upgrade(req, {
942
+ data: buildResponsesWsData(
943
+ selectForwardHeaders(req.headers),
944
+ admission,
945
+ websocketLease,
946
+ sessionLaneIdFromRequest(req.headers),
947
+ ),
438
948
  })) return undefined as unknown as Response;
439
949
  websocketLease.release();
440
- return withCors(formatErrorResponse(426, "upgrade_required", "WebSocket upgrade failed"), req, config);
950
+ return withCors(formatErrorResponse(426, "upgrade_required", "WebSocket upgrade failed"), req, policy);
441
951
  }
442
952
 
443
953
  if (url.pathname === "/healthz" && req.method === "GET") {
444
954
  // service/pid/port let CLI liveness reject foreign 200s and verify pid identity.
445
- return jsonResponse({ status: "ok", service: "opencodex", version: VERSION, uptime: process.uptime(), pid: process.pid, port: listenPort }, 200, req, config);
955
+ const healthPort = server.port ?? listenPort;
956
+ const response = jsonResponse({
957
+ status: "ok",
958
+ service: "opencodex",
959
+ version: VERSION,
960
+ uptime: process.uptime(),
961
+ pid: process.pid,
962
+ port: healthPort,
963
+ restartCapability: SYSTEM_RESTART_CAPABILITY_VERSION,
964
+ providerReloadCapability: LOCAL_PROVIDER_RELOAD_CAPABILITY_VERSION,
965
+ }, 200, req, policy);
966
+ const challenge = req.headers.get(LOCAL_ATTESTATION_CHALLENGE_HEADER);
967
+ if (challenge) {
968
+ const proof = createLocalAttestationProof(localAttestationSecret, challenge, process.pid, healthPort);
969
+ if (proof) response.headers.set(LOCAL_ATTESTATION_PROOF_HEADER, proof);
970
+ }
971
+ return response;
972
+ }
973
+
974
+ // Readiness: like /healthz this is exact GET and unauthenticated (so a client can
975
+ // back off BEFORE knowing the admission token), but stricter than liveness. The
976
+ // body carries only sanitized identity + the fixed status enum; the sync message,
977
+ // warning text, catalog path, provider output, and account data are never exposed.
978
+ // POST or "/readyz/" must NOT match (exact pathname + GET method): answer them
979
+ // with a JSON 404 here so they can never be silently accepted by the GUI SPA
980
+ // fallback (which would serve index.html with HTTP 200 once gui/dist exists).
981
+ if (readyzPath !== undefined) {
982
+ if (readyzPath !== "/readyz" || req.method !== "GET") {
983
+ return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, policy);
984
+ }
985
+ // A draining proxy must never advertise ready: every data-plane branch
986
+ // answers drainingResponse while isDraining() is set, but the one-shot
987
+ // readiness gate is not mutated on shutdown (it is owned by the startup
988
+ // sync). Report pending so `ocx ready --wait` and external supervisors
989
+ // keep polling instead of promoting a proxy that is draining.
990
+ const status = isDraining() ? "pending" : readinessGate.getStatus();
991
+ const body = {
992
+ service: "opencodex",
993
+ version: VERSION,
994
+ uptime: process.uptime(),
995
+ pid: process.pid,
996
+ port: boundPort ?? listenPort,
997
+ status,
998
+ };
999
+ if (status === "ready") {
1000
+ return jsonResponse(body, 200, req, policy);
1001
+ }
1002
+ // Pending/failed: 503 with a conservative Retry-After so well-behaved clients
1003
+ // (and `ocx ready --wait`) back off instead of hot-looping.
1004
+ const resp = jsonResponse(body, 503, req, policy);
1005
+ const headers = new Headers(resp.headers);
1006
+ headers.set("Retry-After", "1");
1007
+ return new Response(resp.body, { status: 503, headers });
446
1008
  }
447
1009
 
448
1010
  if (url.pathname.startsWith("/api/")) {
449
- const apiAuthError = requireManagementAuth(req, managementAuth, config);
1011
+ const localManagementAuth = {
1012
+ attestationSecret: localAttestationSecret,
1013
+ pid: process.pid,
1014
+ port: boundPort ?? requestServer.port ?? listenPort,
1015
+ };
1016
+ const apiAuthError = requireManagementAuth(req, managementAuth, config, localManagementAuth);
450
1017
  if (apiAuthError) return withManagementCors(apiAuthError, req, config);
451
- const mgmtResponse = await handleManagementAPI(req, url, config);
1018
+ // Which credential passed the gate, resolved from the same session table the
1019
+ // gate used. Consent-bearing routes need this: request headers are forgeable
1020
+ // by anything holding the admin token, the credential is not.
1021
+ const principal = managementPrincipal(req, managementAuth, config, localManagementAuth) ?? undefined;
1022
+ const mgmtResponse = await handleManagementAPI(req, url, config, deps.managementApi, principal);
452
1023
  if (mgmtResponse) return withManagementCors(mgmtResponse, req, config);
453
1024
  return withManagementCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, config);
454
1025
  }
@@ -457,25 +1028,80 @@ export function startServer(port?: number) {
457
1028
  // Model discovery never forwards Authorization upstream, so the broader admission
458
1029
  // set (Authorization / x-api-key / x-opencodex-api-key) is safe here and required by
459
1030
  // remote OpenAI-style bearer clients and Claude gateway discovery (anthropic-version).
460
- const admission = resolveApiAuth(req, config);
461
- if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
462
- if (!isAllowedRequestOrigin(req, config)) {
463
- return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
1031
+ const admission = resolveApiAuth(req, policy);
1032
+ if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
1033
+ if (!isAllowedRequestOrigin(req, policy)) {
1034
+ return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
464
1035
  }
465
1036
  let goModels;
1037
+ let modelEntitlements;
466
1038
  try {
467
- goModels = await fetchAllModels(config);
1039
+ [goModels, modelEntitlements] = await Promise.all([
1040
+ fetchAllModels(config),
1041
+ resolveCodexModelEntitlements(config),
1042
+ ]);
468
1043
  } catch (error) {
469
1044
  if (error instanceof CatalogGatherBusyError) {
470
1045
  return withCors(new Response(JSON.stringify({ error: { type: "server_error", code: "catalog_busy", message: error.message } }), {
471
1046
  status: 503,
472
1047
  headers: { "content-type": "application/json", "Retry-After": "1" },
473
- }), req, config);
1048
+ }), req, policy);
474
1049
  }
475
1050
  throw error;
476
1051
  }
477
- const { applyNativeVisibility, buildCatalogEntries, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, nativeOpenAiSlugs, nativeReasoningEfforts, nativeDefaultReasoningEffort, orderForSubagents, filterCatalogVisibleModels, uniqueCatalogModelsForRawPublicList, visibleNativeSlugs, desktopVisibleNativeSlugs } = await import("../codex/catalog");
478
- const nativeSlugs = nativeOpenAiSlugs();
1052
+ const { accountBoundNativeOpenAiSlugsBySelector, applyNativeVisibility, buildCatalogEntries, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, NATIVE_OPENAI_MODELS, nativeContextLimits, nativeOpenAiSlugs, nativeReasoningEfforts, nativeDefaultReasoningEffort, orderForSubagents, filterCatalogVisibleModels, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, uniqueCatalogModelsForRawPublicList, visibleCodexAccountSelectors, visibleNativeSlugs, desktopVisibleNativeSlugs } = await import("../codex/catalog");
1053
+ const { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } = await import("../codex/catalog/native-models");
1054
+ const includeNativeOpenAi = shouldIncludeNativeOpenAi(config);
1055
+ const includeAccountBoundNativeOpenAi = shouldIncludeAccountBoundNativeOpenAi(config);
1056
+ const bareEligibleAccountIds = providerCodexAccountMode(
1057
+ OPENAI_CODEX_PROVIDER_ID,
1058
+ config.providers[OPENAI_CODEX_PROVIDER_ID],
1059
+ ) === "direct" ? new Set([MAIN_CODEX_ACCOUNT_ID]) : undefined;
1060
+ const availableBareGatedNativeSlugs = availableAccountGatedNativeModels(
1061
+ modelEntitlements,
1062
+ bareEligibleAccountIds,
1063
+ );
1064
+ const availableAccountGatedNativeSlugs = availableAccountGatedNativeModels(modelEntitlements);
1065
+ const availableBareNativeSlugs = NATIVE_OPENAI_MODELS.filter(slug => (
1066
+ !ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableBareGatedNativeSlugs.has(slug)
1067
+ ));
1068
+ const availableAccountNativeSlugs = NATIVE_OPENAI_MODELS.filter(slug => (
1069
+ !ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableAccountGatedNativeSlugs.has(slug)
1070
+ ));
1071
+ const nativeSlugs = includeNativeOpenAi
1072
+ ? nativeOpenAiSlugs().filter(slug => (
1073
+ !ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableBareGatedNativeSlugs.has(slug)
1074
+ ))
1075
+ : [];
1076
+ const disabledNatives = disabledNativeSlugs(config);
1077
+ const disabledModels = new Set(config.disabledModels ?? []);
1078
+ const exactComboSlugs = exactComboCatalogSlugs(config);
1079
+ const shadowedNativeSlugs = configuredNativeAliasSlugs(config);
1080
+ const suppressedBareNativeSlugs = new Set([
1081
+ ...desktopAllowlistSuppressedNativeSlugs(config),
1082
+ ...[...ACCOUNT_GATED_NATIVE_OPENAI_MODELS].filter(slug => !availableBareGatedNativeSlugs.has(slug)),
1083
+ ]);
1084
+ const accountSelectors = includeAccountBoundNativeOpenAi
1085
+ ? visibleCodexAccountSelectors(config)
1086
+ : [];
1087
+ const accountTargets = new Map(codexAccountNamespaceEntries(config));
1088
+ const accountNativeSlugsBySelector = includeAccountBoundNativeOpenAi
1089
+ ? new Map([...accountBoundNativeOpenAiSlugsBySelector(config)].map(([selector, slugs]) => {
1090
+ const target = accountTargets.get(selector);
1091
+ const accountId = target && isMainCodexAccountTarget(target) ? MAIN_CODEX_ACCOUNT_ID : target;
1092
+ const entitled = accountId ? modelEntitlements.modelsByAccount.get(accountId) : undefined;
1093
+ const confirmed = accountId ? modelEntitlements.confirmedAccountIds.has(accountId) : false;
1094
+ return [selector, slugs.filter(slug => (
1095
+ !ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || (confirmed && entitled?.has(slug) === true)
1096
+ ))] as const;
1097
+ }))
1098
+ : new Map<string, readonly string[]>();
1099
+ const accountNativeSlugs = [...new Set(
1100
+ [...accountNativeSlugsBySelector.values()].flatMap(slugs => [...slugs]),
1101
+ )];
1102
+ const desktopNativeSlugs = desktopVisibleNativeSlugs(config).filter(slug => (
1103
+ !ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableBareGatedNativeSlugs.has(slug)
1104
+ ));
479
1105
  const goEnabled = filterCatalogVisibleModels(goModels, config);
480
1106
  const goOrdered = orderForSubagents(goEnabled, config.subagentModels);
481
1107
  // Claude Code / Claude Desktop gateway model discovery (GET /v1/models with
@@ -489,10 +1115,10 @@ export function startServer(port?: number) {
489
1115
  const wantsAnthropicList = req.headers.get("anthropic-version") !== null
490
1116
  || url.searchParams.get("flavor") === "anthropic";
491
1117
  if (wantsAnthropicList && !url.searchParams.has("client_version")) {
492
- if (config.claudeCode?.enabled === false) return jsonResponse({ data: [] }, 200, req, config);
1118
+ if (config.claudeCode?.enabled === false) return jsonResponse({ data: [] }, 200, req, policy);
493
1119
  // Build Desktop 3P registry so inbound alias resolution works for subsequent requests.
494
1120
  buildDesktop3pRegistry(
495
- [...desktopVisibleNativeSlugs(config)],
1121
+ desktopNativeSlugs,
496
1122
  goOrdered.map(m => ({ provider: m.provider, id: m.id, contextWindow: m.contextWindow })),
497
1123
  config.claudeCode?.desktopProfile,
498
1124
  );
@@ -509,8 +1135,8 @@ export function startServer(port?: number) {
509
1135
  : idsParam === "desktop"
510
1136
  ? "desktop3p" as const
511
1137
  : (/^claude-code\//i.test(req.headers.get("user-agent") ?? "") ? "readable" as const : "desktop3p" as const);
512
- const data = buildAnthropicModelInfos([...desktopVisibleNativeSlugs(config)], goOrdered, resolveAutoContext(config.claudeCode), idStyle, activeDesktop3pAlias);
513
- return jsonResponse({ data }, 200, req, config);
1138
+ const data = buildAnthropicModelInfos(desktopNativeSlugs, goOrdered, resolveAutoContext(config.claudeCode), idStyle, activeDesktop3pAlias, nativeContextLimits(config));
1139
+ return jsonResponse({ data }, 200, req, policy);
514
1140
  }
515
1141
  if (url.searchParams.has("client_version")) {
516
1142
  // Codex client → Codex catalog shape: native gpt + namespaced routed models,
@@ -519,8 +1145,39 @@ export function startServer(port?: number) {
519
1145
  // Disabled natives stay in the catalog shape with visibility "hide" (mirrors the
520
1146
  // on-disk sync; codex-rs keeps them out of the picker itself).
521
1147
  const maMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2" ? config.multiAgentMode : "default";
522
- const entries = buildCatalogEntries(loadCatalogTemplate(), nativeSlugs, goOrdered, config.subagentModels, websocketsEnabled(config), maMode as "v1" | "default" | "v2", exactComboCatalogSlugs(config));
523
- return jsonResponse({ models: applyNativeVisibility(entries, disabledNativeSlugs(config)) }, 200, req, config);
1148
+ // Account rows use the same hidden-inclusive supported set as on-disk sync. This lets a
1149
+ // newly re-enabled native reappear under each selector before the next sync, while the
1150
+ // no-selector path keeps nativeOpenAiSlugs()'s existing visibility-sensitive behavior.
1151
+ const catalogNativeSlugs = accountSelectors.length > 0
1152
+ ? [...new Set([
1153
+ ...availableAccountNativeSlugs,
1154
+ ...accountNativeSlugs,
1155
+ ])]
1156
+ : nativeSlugs;
1157
+ const entries = buildCatalogEntries(
1158
+ loadCatalogTemplate(),
1159
+ catalogNativeSlugs,
1160
+ goOrdered,
1161
+ config.subagentModels,
1162
+ websocketsEnabled(config),
1163
+ maMode as "v1" | "default" | "v2",
1164
+ exactComboSlugs,
1165
+ accountSelectors,
1166
+ suppressedBareNativeSlugs,
1167
+ new Set(),
1168
+ nativeContextLimits(config),
1169
+ accountNativeSlugs,
1170
+ accountNativeSlugsBySelector,
1171
+ config.keepNativeChatGptOnV1 === true,
1172
+ );
1173
+ return jsonResponse({
1174
+ models: applyNativeVisibility(
1175
+ entries,
1176
+ disabledModels,
1177
+ accountSelectors.length > 0,
1178
+ new Set(accountNativeSlugs),
1179
+ ),
1180
+ }, 200, req, policy);
524
1181
  }
525
1182
  // OpenAI list shape: native gpt bare + routed models namespaced "<provider>/<id>"
526
1183
  // (pure availability list — disabled natives are omitted entirely).
@@ -548,23 +1205,65 @@ export function startServer(port?: number) {
548
1205
  reasoning_efforts: efforts.map(effort => grokEffortOption(effort, effort === defaultEffort)),
549
1206
  };
550
1207
  };
551
- const data = [
552
- ...visibleNativeSlugs(config).map(id => ({
1208
+ const nativeModelRow = (id: string, metadataId = id) => ({
553
1209
  id,
554
1210
  object: "model",
555
1211
  created: 0,
556
1212
  owned_by: "openai",
557
- ...grokEffortFields(nativeReasoningEfforts(id), nativeDefaultReasoningEffort(id)),
558
- })),
559
- ...uniqueCatalogModelsForRawPublicList(goOrdered).map(m => ({
560
- id: m.alias ?? `${m.provider}/${m.id}`,
561
- object: "model",
562
- created: 0,
563
- owned_by: m.owned_by ?? m.provider,
564
- ...grokEffortFields(m.reasoningEfforts ?? [], m.defaultReasoningEffort),
1213
+ ...grokEffortFields(
1214
+ nativeReasoningEfforts(metadataId),
1215
+ nativeDefaultReasoningEffort(metadataId),
1216
+ ),
1217
+ });
1218
+ // Selector-active discovery follows the same complete supported set as the Codex catalog
1219
+ // for both bare and qualified rows. Without selectors, the live catalog continues to own
1220
+ // bare availability.
1221
+ const selectorNativeSlugs = accountSelectors.length > 0
1222
+ ? availableBareNativeSlugs.filter(slug => !disabledNatives.has(slug))
1223
+ : [];
1224
+ const bareSelectorNativeSlugs = accountSelectors.length > 0
1225
+ ? selectorNativeSlugs
1226
+ : [];
1227
+ const visibleNatives = includeNativeOpenAi
1228
+ ? accountSelectors.length > 0
1229
+ ? bareSelectorNativeSlugs.filter(slug => !shadowedNativeSlugs.has(slug))
1230
+ : visibleNativeSlugs(config)
1231
+ : [];
1232
+ const visibleAccountNatives = accountSelectors.flatMap(selector =>
1233
+ (accountNativeSlugsBySelector.get(selector) ?? []).filter(metadataId => !disabledNatives.has(metadataId)).flatMap(metadataId => {
1234
+ const id = `${selector}/${metadataId}`;
1235
+ return disabledModels.has(id) ? [] : [{ id, metadataId }];
1236
+ })
1237
+ );
1238
+ const data = [
1239
+ ...visibleNatives.map(id => nativeModelRow(id)),
1240
+ ...visibleAccountNatives.map(({ id, metadataId }) => nativeModelRow(id, metadataId)),
1241
+ ...await Promise.all(uniqueCatalogModelsForRawPublicList(goOrdered).map(async m => {
1242
+ const publicId = m.alias ?? `${m.provider}/${m.id}`;
1243
+ const isCombo = m.provider === "combo" && exactComboSlugs.has(publicId);
1244
+ const provider = config.providers[m.provider];
1245
+ const effective = provider
1246
+ ? (await import("../providers/default-aliases")).effectiveModelAliases(
1247
+ config,
1248
+ provider,
1249
+ knownModelIdsForProvider(m.provider, provider, config),
1250
+ ).get(m.id)
1251
+ : undefined;
1252
+ return {
1253
+ id: publicId,
1254
+ object: "model",
1255
+ created: 0,
1256
+ // This endpoint is an OpenAI-compatible inbound contract. Some clients use
1257
+ // owned_by as an adapter selector, so a virtual combo must name that wire
1258
+ // adapter rather than the internal catalog authority marker.
1259
+ owned_by: isCombo ? "openai" : (m.owned_by ?? m.provider),
1260
+ ...(isCombo ? { is_combo: true } : {}),
1261
+ ...(effective ? { alias_of: `${provider?.alias || m.provider}/${effective.alias}` } : {}),
1262
+ ...grokEffortFields(m.reasoningEfforts ?? [], m.defaultReasoningEffort),
1263
+ };
565
1264
  })),
566
1265
  ];
567
- return jsonResponse({ object: "list", data }, 200, req, config);
1266
+ return jsonResponse({ object: "list", data }, 200, req, policy);
568
1267
  }
569
1268
 
570
1269
  // Remote compaction v1 (codex-rs with Feature::RemoteCompactionV2 off — the default).
@@ -572,12 +1271,12 @@ export function startServer(port?: number) {
572
1271
  // before the /v1/* 404 guard below.
573
1272
  if (url.pathname === "/v1/responses/compact" && req.method === "POST") {
574
1273
  if (isDraining()) {
575
- return drainingResponse(req);
1274
+ return drainingResponse(req, policy);
576
1275
  }
577
- const admission = resolveResponsesApiAuth(req, config);
578
- if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
579
- if (!isAllowedRequestOrigin(req, config)) {
580
- return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
1276
+ const admission = resolveResponsesApiAuth(req, policy);
1277
+ if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
1278
+ if (!isAllowedRequestOrigin(req, policy)) {
1279
+ return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
581
1280
  }
582
1281
  const start = Date.now();
583
1282
  const requestId = nextRequestLogId(start);
@@ -587,16 +1286,16 @@ export function startServer(port?: number) {
587
1286
  ...admissionFields(admission),
588
1287
  inboundProtocol: "responses",
589
1288
  };
590
- return runAdmittedHttpTurn(req, async () => {
1289
+ return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
591
1290
  let response: Response;
592
1291
  try {
593
- response = await handleResponsesCompact(req, config, logCtx);
1292
+ response = await handleResponsesCompact(req, config, logCtx, turnAdmissionLease, admission);
594
1293
  } catch {
595
1294
  response = formatErrorResponse(500, "server_error", "Unexpected compact request failure");
596
1295
  }
597
1296
  addFinalRequestLog(requestId, start, logCtx, response.status,
598
1297
  response.status === 499 ? { closeReason: "client_cancel" } : undefined);
599
- return withCors(response, req, config);
1298
+ return withCors(response, req, policy);
600
1299
  });
601
1300
  }
602
1301
 
@@ -606,12 +1305,12 @@ export function startServer(port?: number) {
606
1305
  ) {
607
1306
  disableResponsesRequestTimeout(req, requestServer);
608
1307
  if (isDraining()) {
609
- return drainingResponse(req);
1308
+ return drainingResponse(req, policy);
610
1309
  }
611
- const admission = resolveApiAuth(req, config);
612
- if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
613
- if (!isAllowedRequestOrigin(req, config)) {
614
- return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
1310
+ const admission = resolveApiAuth(req, policy);
1311
+ if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
1312
+ if (!isAllowedRequestOrigin(req, policy)) {
1313
+ return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
615
1314
  }
616
1315
  const start = Date.now();
617
1316
  const requestId = nextRequestLogId(start);
@@ -621,24 +1320,24 @@ export function startServer(port?: number) {
621
1320
  ...admissionFields(admission),
622
1321
  };
623
1322
  const endpoint = url.pathname.endsWith("/edits") ? "edits" as const : "generations" as const;
624
- return runAdmittedHttpTurn(req, async () => {
625
- const response = await handleImages(req, config, endpoint, logCtx);
1323
+ return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
1324
+ const response = await handleImages(req, config, endpoint, logCtx, turnAdmissionLease);
626
1325
  addFinalRequestLog(requestId, start, logCtx, response.status, response.status === 499 ? { closeReason: "client_cancel" } : undefined);
627
- return withCors(response, req, config);
1326
+ return withCors(response, req, policy);
628
1327
  });
629
1328
  }
630
1329
 
631
1330
  if (req.method === "GET" && url.pathname.startsWith("/v1/opencodex/artifacts/")) {
632
- const admission = resolveApiAuth(req, config);
633
- if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
634
- if (!isAllowedRequestOrigin(req, config)) {
635
- return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
1331
+ const admission = resolveApiAuth(req, policy);
1332
+ if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
1333
+ if (!isAllowedRequestOrigin(req, policy)) {
1334
+ return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
636
1335
  }
637
1336
  const id = decodeURIComponent(url.pathname.slice("/v1/opencodex/artifacts/".length));
638
1337
  const { resolveArtifactPath } = await import("../images/artifacts");
639
1338
  const artifactPath = resolveArtifactPath(id);
640
1339
  if (!artifactPath) {
641
- return withCors(formatErrorResponse(404, "not_found", "artifact not found"), req, config);
1340
+ return withCors(formatErrorResponse(404, "not_found", "artifact not found"), req, policy);
642
1341
  }
643
1342
  const file = Bun.file(artifactPath);
644
1343
  const ext = artifactPath.split(".").pop()?.toLowerCase();
@@ -655,18 +1354,18 @@ export function startServer(port?: number) {
655
1354
  "cache-control": "private, max-age=3600",
656
1355
  "x-content-type-options": "nosniff",
657
1356
  },
658
- }), req, config);
1357
+ }), req, policy);
659
1358
  }
660
1359
 
661
1360
  if (url.pathname === "/v1/alpha/search" && req.method === "POST") {
662
1361
  disableResponsesRequestTimeout(req, requestServer);
663
1362
  if (isDraining()) {
664
- return drainingResponse(req);
1363
+ return drainingResponse(req, policy);
665
1364
  }
666
- const admission = resolveApiAuth(req, config);
667
- if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
668
- if (!isAllowedRequestOrigin(req, config)) {
669
- return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
1365
+ const admission = resolveApiAuth(req, policy);
1366
+ if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
1367
+ if (!isAllowedRequestOrigin(req, policy)) {
1368
+ return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
670
1369
  }
671
1370
  const start = Date.now();
672
1371
  const requestId = nextRequestLogId(start);
@@ -675,23 +1374,22 @@ export function startServer(port?: number) {
675
1374
  provider: "unknown",
676
1375
  ...admissionFields(admission),
677
1376
  };
678
- return runAdmittedHttpTurn(req, async () => {
679
- const response = await handleSearch(req, config, logCtx);
1377
+ return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
1378
+ const response = await handleSearch(req, config, logCtx, turnAdmissionLease);
680
1379
  addFinalRequestLog(requestId, start, logCtx, response.status,
681
1380
  response.status === 499 ? { closeReason: "client_cancel" } : undefined);
682
- return withCors(response, req, config);
1381
+ return withCors(response, req, policy);
683
1382
  });
684
1383
  }
685
1384
 
686
1385
  if (url.pathname === "/v1/responses" && req.method === "POST") {
687
- disableResponsesRequestTimeout(req, requestServer);
688
1386
  if (isDraining()) {
689
- return drainingResponse(req);
1387
+ return drainingResponse(req, policy);
690
1388
  }
691
- const admission = resolveResponsesApiAuth(req, config);
692
- if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
693
- if (!isAllowedRequestOrigin(req, config)) {
694
- return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
1389
+ const admission = resolveResponsesApiAuth(req, policy);
1390
+ if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
1391
+ if (!isAllowedRequestOrigin(req, policy)) {
1392
+ return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
695
1393
  }
696
1394
  const start = Date.now();
697
1395
  const requestId = nextRequestLogId(start);
@@ -701,6 +1399,7 @@ export function startServer(port?: number) {
701
1399
  ...admissionFields(admission),
702
1400
  inboundProtocol: "responses",
703
1401
  };
1402
+ if (req.headers.get("x-opencodex-grok") === "1") logCtx.surface = "grok";
704
1403
  let logged = false;
705
1404
  const finalizeNativePassthroughLog = (
706
1405
  status: number,
@@ -710,13 +1409,15 @@ export function startServer(port?: number) {
710
1409
  logged = true;
711
1410
  addFinalRequestLog(requestId, start, logCtx, status, meta);
712
1411
  };
713
- return runAdmittedHttpTurn(req, async turnAdmissionLease => {
1412
+ return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
714
1413
  const response = await handleResponses(req, config, logCtx, {
715
1414
  turnAdmissionLease,
1415
+ admission,
1416
+ onRequestBodyRead: () => disableResponsesRequestTimeout(req, requestServer),
716
1417
  abortSignal: req.signal,
717
1418
  onFirstOutput: () => recordFirstOutput(logCtx, start),
718
1419
  onNativePassthroughTerminal: status => {
719
- finalizeNativePassthroughLog(httpStatusForTerminalStatus(status), {
1420
+ finalizeNativePassthroughLog(httpStatusForRequestLogTerminal(status, logCtx), {
720
1421
  terminalStatus: status,
721
1422
  closeReason: "terminal",
722
1423
  });
@@ -725,7 +1426,7 @@ export function startServer(port?: number) {
725
1426
  finalizeNativePassthroughLog(499, { closeReason: "client_cancel" });
726
1427
  },
727
1428
  });
728
- return withCors(responseWithDeferredRequestLog(response, requestId, start, logCtx), req, config);
1429
+ return withCors(responseWithDeferredRequestLog(response, requestId, start, logCtx), req, policy);
729
1430
  });
730
1431
  }
731
1432
 
@@ -733,29 +1434,33 @@ export function startServer(port?: number) {
733
1434
  // Claude Code posts `/v1/messages?beta=true` — pathname match ignores the query (003 G9).
734
1435
  if (url.pathname === "/v1/messages/count_tokens" && req.method === "POST") {
735
1436
  if (isDraining()) {
736
- return drainingResponse(req);
1437
+ return drainingResponse(req, policy);
737
1438
  }
738
- const admission = resolveApiAuth(req, config);
1439
+ const admission = resolveApiAuth(req, policy);
739
1440
  if (!admission) {
740
- return withCors(anthropicErrorResponse(401, "opencodex API key required", "authentication_error"), req, config);
1441
+ return withCors(anthropicErrorResponse(401, "opencodex API key required", "authentication_error"), req, policy);
741
1442
  }
742
- if (!isAllowedRequestOrigin(req, config)) {
743
- return withCors(anthropicErrorResponse(403, "cross-origin data-plane request blocked", "permission_error"), req, config);
1443
+ if (!isAllowedRequestOrigin(req, policy)) {
1444
+ return withCors(anthropicErrorResponse(403, "cross-origin data-plane request blocked", "permission_error"), req, policy);
744
1445
  }
745
- return runAdmittedHttpTurn(req, async () => withCors(await handleClaudeCountTokens(req, config), req, config));
1446
+ return runAdmittedHttpTurn(req, policy, async () => withCors(
1447
+ await handleClaudeCountTokens(req, config, policy),
1448
+ req,
1449
+ policy,
1450
+ ));
746
1451
  }
747
1452
 
748
1453
  if (url.pathname === "/v1/messages" && req.method === "POST") {
749
1454
  disableResponsesRequestTimeout(req, requestServer);
750
1455
  if (isDraining()) {
751
- return drainingResponse(req);
1456
+ return drainingResponse(req, policy);
752
1457
  }
753
- const admission = resolveApiAuth(req, config);
1458
+ const admission = resolveApiAuth(req, policy);
754
1459
  if (!admission) {
755
- return withCors(anthropicErrorResponse(401, "opencodex API key required", "authentication_error"), req, config);
1460
+ return withCors(anthropicErrorResponse(401, "opencodex API key required", "authentication_error"), req, policy);
756
1461
  }
757
- if (!isAllowedRequestOrigin(req, config)) {
758
- return withCors(anthropicErrorResponse(403, "cross-origin data-plane request blocked", "permission_error"), req, config);
1462
+ if (!isAllowedRequestOrigin(req, policy)) {
1463
+ return withCors(anthropicErrorResponse(403, "cross-origin data-plane request blocked", "permission_error"), req, policy);
759
1464
  }
760
1465
  const start = Date.now();
761
1466
  const requestId = nextRequestLogId(start);
@@ -768,10 +1473,10 @@ export function startServer(port?: number) {
768
1473
  // Logging is finalized inside handleClaudeMessages (Responses-vocab tap on the
769
1474
  // pre-translation stream + native passthrough callbacks) — do not re-wrap the
770
1475
  // translated Anthropic stream here.
771
- return runAdmittedHttpTurn(req, async turnAdmissionLease => withCors(
772
- await handleClaudeMessages(req, config, logCtx, { requestId, start, turnAdmissionLease }),
1476
+ return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => withCors(
1477
+ await handleClaudeMessages(req, config, logCtx, { requestId, start, turnAdmissionLease }, policy),
773
1478
  req,
774
- config,
1479
+ policy,
775
1480
  ));
776
1481
  }
777
1482
 
@@ -780,12 +1485,12 @@ export function startServer(port?: number) {
780
1485
  if (url.pathname === "/v1/chat/completions" && req.method === "POST") {
781
1486
  disableResponsesRequestTimeout(req, requestServer);
782
1487
  if (isDraining()) {
783
- return drainingResponse(req);
1488
+ return drainingResponse(req, policy);
784
1489
  }
785
- const admission = resolveResponsesApiAuth(req, config);
786
- if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
787
- if (!isAllowedRequestOrigin(req, config)) {
788
- return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
1490
+ const admission = resolveResponsesApiAuth(req, policy);
1491
+ if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
1492
+ if (!isAllowedRequestOrigin(req, policy)) {
1493
+ return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
789
1494
  }
790
1495
  const start = Date.now();
791
1496
  const requestId = nextRequestLogId(start);
@@ -795,8 +1500,8 @@ export function startServer(port?: number) {
795
1500
  ...admissionFields(admission),
796
1501
  inboundProtocol: "chat",
797
1502
  };
798
- return runAdmittedHttpTurn(req, async turnAdmissionLease => withCors(
799
- await handleChatCompletions(req, config, logCtx, { requestId, start, turnAdmissionLease }),
1503
+ return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => withCors(
1504
+ await handleChatCompletions(req, config, logCtx, { requestId, start, turnAdmissionLease, admission }),
800
1505
  req,
801
1506
  config,
802
1507
  ));
@@ -811,12 +1516,12 @@ export function startServer(port?: number) {
811
1516
  ) {
812
1517
  disableResponsesRequestTimeout(req, requestServer);
813
1518
  if (isDraining()) {
814
- return drainingResponse(req);
1519
+ return drainingResponse(req, policy);
815
1520
  }
816
- const admission = resolveApiAuth(req, config);
817
- if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
818
- if (!isAllowedRequestOrigin(req, config)) {
819
- return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
1521
+ const admission = resolveApiAuth(req, policy);
1522
+ if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
1523
+ if (!isAllowedRequestOrigin(req, policy)) {
1524
+ return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
820
1525
  }
821
1526
  const start = Date.now();
822
1527
  const requestId = nextRequestLogId(start);
@@ -825,8 +1530,8 @@ export function startServer(port?: number) {
825
1530
  provider: "unknown",
826
1531
  ...admissionFields(admission),
827
1532
  };
828
- return runAdmittedHttpTurn(req, async () => {
829
- const response = await handleLive(req, config, logCtx);
1533
+ return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
1534
+ const response = await handleLive(req, config, logCtx, turnAdmissionLease);
830
1535
  addFinalRequestLog(
831
1536
  requestId,
832
1537
  start,
@@ -834,23 +1539,26 @@ export function startServer(port?: number) {
834
1539
  response.status,
835
1540
  response.status === 499 ? { closeReason: "client_cancel" } : undefined,
836
1541
  );
837
- return withCors(response, req, config);
1542
+ return withCors(response, req, policy);
838
1543
  });
839
1544
  }
840
1545
 
841
- // Voice / Realtime sideband WebSocket: Frameless joins /v1/live/{callId}; Realtime v1 joins
842
- // /v1/realtime?call_id= (or /v1/realtime/calls/{callId}). Transparent bidirectional relay.
1546
+ // Voice / Realtime WebSocket relay. Sideband joins: Frameless /v1/live/{callId};
1547
+ // Realtime v1 /v1/realtime?call_id= (or /v1/realtime/calls/{callId}). Standalone
1548
+ // sessions (codex-rs thread/realtime/start, WebSocket transport — the desktop voice
1549
+ // path): /v1/realtime?intent=quicksilver&model= and /v1/live?model=.
1550
+ // Transparent bidirectional relay.
843
1551
  const liveSidebandTarget = req.headers.get("upgrade")?.toLowerCase() === "websocket"
844
- ? parseLiveSidebandTarget(url.pathname, url.searchParams)
1552
+ ? parseLiveSidebandTarget(url.pathname, url.searchParams, url.search.replace(/^\?/, ""))
845
1553
  : null;
846
1554
  if (liveSidebandTarget) {
847
1555
  if (isDraining()) {
848
- return drainingResponse(req);
1556
+ return drainingResponse(req, policy);
849
1557
  }
850
- const admission = resolveApiAuth(req, config);
851
- if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
852
- if (!isAllowedRequestOrigin(req, config)) {
853
- return withCors(formatErrorResponse(403, "origin_rejected", "WebSocket upgrade blocked: non-local Origin"), req, config);
1558
+ const admission = resolveApiAuth(req, policy);
1559
+ if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
1560
+ if (!isAllowedRequestOrigin(req, policy)) {
1561
+ return withCors(formatErrorResponse(403, "origin_rejected", "WebSocket upgrade blocked: non-local Origin"), req, policy);
854
1562
  }
855
1563
  const start = Date.now();
856
1564
  const requestId = nextRequestLogId(start);
@@ -859,22 +1567,34 @@ export function startServer(port?: number) {
859
1567
  provider: "unknown",
860
1568
  ...admissionFields(admission),
861
1569
  };
862
- const resolved = await resolveLiveSidebandUpgrade(req, config, logCtx, liveSidebandTarget);
1570
+ const turnAdmissionLease = tryAdmitTurn(sessionLaneIdFromRequest(req.headers));
1571
+ if (!turnAdmissionLease) return serverBusyResponse(req, "active turns", policy);
1572
+ let resolved;
1573
+ try {
1574
+ resolved = await resolveLiveSidebandUpgrade(req, config, logCtx, liveSidebandTarget, turnAdmissionLease);
1575
+ } catch (error) {
1576
+ turnAdmissionLease.release();
1577
+ throw error;
1578
+ }
863
1579
  if (resolved instanceof Response) {
1580
+ turnAdmissionLease.release();
864
1581
  addFinalRequestLog(requestId, start, logCtx, resolved.status);
865
- return withCors(resolved, req, config);
1582
+ return withCors(resolved, req, policy);
866
1583
  }
867
1584
  addFinalRequestLog(requestId, start, logCtx, 101);
868
- if (server.upgrade(req, {
1585
+ if (requestServer.upgrade(req, {
869
1586
  data: {
870
1587
  kind: "live-sideband",
871
1588
  liveUpstreamUrl: resolved.upstreamWsUrl,
872
1589
  liveUpstreamHeaders: resolved.headers,
873
1590
  livePending: [],
1591
+ livePendingBytes: 0,
874
1592
  liveOpened: false,
1593
+ liveTurnAdmissionLease: turnAdmissionLease,
875
1594
  } satisfies WsData,
876
1595
  })) return undefined as unknown as Response;
877
- return withCors(formatErrorResponse(426, "upgrade_required", "WebSocket upgrade failed"), req, config);
1596
+ turnAdmissionLease.release();
1597
+ return withCors(formatErrorResponse(426, "upgrade_required", "WebSocket upgrade failed"), req, policy);
878
1598
  }
879
1599
 
880
1600
  // Data-plane guard: unknown /v1/* paths must fail with JSON 404, never fall through to the
@@ -882,12 +1602,17 @@ export function startServer(port?: number) {
882
1602
  // endpoint clients — memories/*, realtime/* — would surface confusing
883
1603
  // serde decode errors instead of a clean not-found).
884
1604
  if (url.pathname.startsWith("/v1/")) {
885
- return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, config);
1605
+ return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, policy);
886
1606
  }
887
1607
 
888
1608
  const guiSessionCandidate = req.method === "GET" && (url.pathname === "/" || !url.pathname.includes("."))
889
1609
  ? issueGuiSession(req, config, managementAuth)
890
1610
  : null;
1611
+ // Dedicated bootstrap path: answer without requiring a packaged GUI build, so the
1612
+ // Vite dev server can mint an origin-bound loopback session on a fresh checkout.
1613
+ if (url.pathname === "/opencodex-session" && guiSessionCandidate) {
1614
+ return serveSessionBootstrap(guiSessionCandidate);
1615
+ }
891
1616
  const guiFile = serveGuiFile(url.pathname, undefined, guiSessionCandidate ?? undefined);
892
1617
  if (guiFile) return guiFile;
893
1618
  if (url.pathname === "/" && req.method === "GET") {
@@ -897,6 +1622,7 @@ export function startServer(port?: number) {
897
1622
  return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, config);
898
1623
  },
899
1624
  websocket: {
1625
+ maxPayloadLength: MAX_WS_FRAME_BYTES,
900
1626
  idleTimeout: WEBSOCKET_IDLE_TIMEOUT_SECONDS,
901
1627
  // Responses WebSocket data plane (phase 120.2). Re-frames the same SSE pipeline onto the
902
1628
  // socket: parse response.create → run handleResponses unchanged → pump its SSE body as WS
@@ -904,7 +1630,11 @@ export function startServer(port?: number) {
904
1630
  // Live sideband sockets (kind=live-sideband) are a transparent bidirectional relay instead.
905
1631
  open(ws: ServerWebSocket<WsData>) {
906
1632
  if (ws.data.kind === "live-sideband") {
907
- attachLiveSidebandUpstream(ws);
1633
+ if (!ws.data.liveTurnAdmissionLease) {
1634
+ closeLiveSideband(ws, 1013, "server busy");
1635
+ return;
1636
+ }
1637
+ attachLiveSidebandUpstream(ws, deps.liveSidebandWebSocketFactory);
908
1638
  return;
909
1639
  }
910
1640
  if (!ws.data.admissionLease) {
@@ -916,15 +1646,24 @@ export function startServer(port?: number) {
916
1646
  },
917
1647
  message(ws: ServerWebSocket<WsData>, raw: string | Buffer) {
918
1648
  if (ws.data.kind === "live-sideband") {
1649
+ if (ws.data.liveClosing) return;
1650
+ const rawBytes = webSocketFrameBytes(raw);
1651
+ if (exceedsLiveSidebandFrameByteLimit(rawBytes)) {
1652
+ closeLiveSideband(ws, 1009, "message too large");
1653
+ return;
1654
+ }
919
1655
  logLiveSidebandFrame("c2u", raw);
920
1656
  const upstream = ws.data.liveUpstream;
921
1657
  if (!upstream || upstream.readyState === WebSocket.CONNECTING || !ws.data.liveOpened) {
922
- const pending = ws.data.livePending ?? (ws.data.livePending = []);
923
- if (pending.length >= LIVE_SIDEBAND_PENDING_MAX) {
1658
+ const enqueueResult = enqueueLiveSidebandPendingFrame(ws.data, raw, rawBytes);
1659
+ if (enqueueResult === "too-many-frames") {
924
1660
  closeLiveSideband(ws, 1009, "too many pending frames");
925
1661
  return;
926
1662
  }
927
- pending.push(raw);
1663
+ if (enqueueResult === "too-many-bytes") {
1664
+ closeLiveSideband(ws, 1009, "too many pending bytes");
1665
+ return;
1666
+ }
928
1667
  return;
929
1668
  }
930
1669
  if (upstream.readyState !== WebSocket.OPEN) {
@@ -932,7 +1671,7 @@ export function startServer(port?: number) {
932
1671
  return;
933
1672
  }
934
1673
  try {
935
- upstream.send(raw);
1674
+ sendUpstreamFrame(upstream, raw);
936
1675
  } catch {
937
1676
  closeLiveSideband(ws, 1011, "upstream send failed");
938
1677
  }
@@ -980,7 +1719,7 @@ export function startServer(port?: number) {
980
1719
  return;
981
1720
  }
982
1721
 
983
- const turnAdmissionLease = tryAdmitTurn();
1722
+ const turnAdmissionLease = tryAdmitTurn(ws.data.sessionLaneId);
984
1723
  if (!turnAdmissionLease) {
985
1724
  sendJsonFrame(ws, buildWsErrorFrame(503, {
986
1725
  type: "server_error",
@@ -1029,7 +1768,9 @@ export function startServer(port?: number) {
1029
1768
  try {
1030
1769
  let terminalRecorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined;
1031
1770
  const response = await handleResponses(req, config, logCtx, {
1771
+ ...(wsAdmission ? { admission: wsAdmission } : {}),
1032
1772
  forceEmptyResponseId: true,
1773
+ inboundTransport: "websocket",
1033
1774
  abortSignal: turnAbort.signal,
1034
1775
  turnAdmissionLease,
1035
1776
  onFirstOutput: () => recordFirstOutput(logCtx, start),
@@ -1057,9 +1798,12 @@ export function startServer(port?: number) {
1057
1798
  finalizeLog(429);
1058
1799
  // Codex Desktop rides this WS transport, so it must carry the same
1059
1800
  // actionable text as HTTP; a frame has no headers, hence message-only.
1801
+ const accountSelector = typeof payload.model === "string"
1802
+ ? codexAccountNamespaceForModel(config.codexAccountNamespaces, payload.model)
1803
+ : undefined;
1060
1804
  sendJsonFrame(ws, buildWsErrorFrame(429, {
1061
1805
  type: "rate_limit_error",
1062
- message: cooldownErrorMessage(err),
1806
+ message: cooldownErrorMessage(err, accountSelector),
1063
1807
  }));
1064
1808
  return;
1065
1809
  }
@@ -1080,8 +1824,7 @@ export function startServer(port?: number) {
1080
1824
  },
1081
1825
  close(ws: ServerWebSocket<WsData>) {
1082
1826
  if (ws.data.kind === "live-sideband") {
1083
- ws.data.cancel?.();
1084
- ws.data.liveUpstream = undefined;
1827
+ closeLiveSideband(ws);
1085
1828
  return;
1086
1829
  }
1087
1830
  unregisterCodexWebSocket(ws);
@@ -1090,10 +1833,69 @@ export function startServer(port?: number) {
1090
1833
  ws.data.cancel?.(); // RC2: abort the upstream when the client disconnects
1091
1834
  },
1092
1835
  },
1093
- });
1836
+ } as const;
1837
+
1838
+ server = Bun.serve<WsData>({ ...serveOptions, port: listenPort, hostname: bindHost });
1839
+
1840
+ // Both binds are one startup transaction (#1102). If the loopback bind fails after the
1841
+ // public one succeeded, leaving the public listener up would strand it: the CLI's port
1842
+ // retry would read the failure as a public-port conflict and pick a different port,
1843
+ // accumulating listeners. Roll back and rethrow the original error instead.
1844
+ if (loopbackListenerPort !== null) {
1845
+ try {
1846
+ loopbackServer = Bun.serve<WsData>({
1847
+ ...serveOptions,
1848
+ port: loopbackListenerPort,
1849
+ hostname: "127.0.0.1",
1850
+ });
1851
+ } catch (error) {
1852
+ try {
1853
+ // startServer is synchronous, so this rollback cannot await. Bun begins closing the
1854
+ // listen socket on the call itself; the caller sees the original bind error either
1855
+ // way, and the alternative — leaving the public listener up — is the failure this
1856
+ // rollback exists to prevent.
1857
+ void server.stop(true);
1858
+ } catch {
1859
+ /* the original bind error is the one worth reporting */
1860
+ }
1861
+ throw error;
1862
+ }
1863
+ }
1864
+ } catch (error) {
1865
+ userCostOverlayReconciler?.stop();
1866
+ backgroundLifecycle?.releaseAfterFailedStart();
1867
+ void nativeMainLifecycle.release();
1868
+ throw error;
1869
+ }
1094
1870
 
1871
+ bindNativeMainStartupLifecycle(server, nativeMainLifecycle);
1872
+ const nativeStop = server.stop.bind(server);
1873
+ const loopbackListenerRef = loopbackServer;
1874
+ Object.defineProperty(server, "stop", {
1875
+ configurable: true,
1876
+ value: async (closeActiveConnections?: boolean): Promise<void> => {
1877
+ // The orchestration lives in `runListenerShutdown` so its two competing properties —
1878
+ // cleanup completes, failure propagates — are testable without a live socket.
1879
+ await runListenerShutdown(
1880
+ [
1881
+ () => nativeStop(closeActiveConnections),
1882
+ ...(loopbackListenerRef
1883
+ ? [() => loopbackListenerRef.stop(closeActiveConnections)]
1884
+ : []),
1885
+ async () => {
1886
+ userCostOverlayReconciler?.stop();
1887
+ },
1888
+ ],
1889
+ async () => {
1890
+ await backgroundLifecycle.release();
1891
+ await releaseNativeMainStartupLifecycle(server);
1892
+ },
1893
+ );
1894
+ },
1895
+ });
1095
1896
  setServerRef(server);
1096
1897
  const actualPort = server.port ?? listenPort;
1898
+ boundPort = actualPort;
1097
1899
  setCorsOrigin(actualPort);
1098
1900
 
1099
1901
  console.log(`🚀 opencodex proxy running on http://localhost:${actualPort}`);
@@ -1103,6 +1905,17 @@ export function startServer(port?: number) {
1103
1905
  console.log(` GET /api/* → management API`);
1104
1906
  console.log(` GET / → GUI dashboard`);
1105
1907
 
1908
+ if (loopbackServer) {
1909
+ // Loud on every start, not once at enable time. An operator who inherits a config, or
1910
+ // who forgot, has to be able to see that an unauthenticated surface is live without
1911
+ // reading the file.
1912
+ const loopbackPort = loopbackServer.port ?? loopbackListenerPort;
1913
+ console.warn(`⚠️ Unauthenticated loopback listener active on http://127.0.0.1:${loopbackPort}`);
1914
+ console.warn(` Any local process can use it without a credential — it spends account`);
1915
+ console.warn(` quota and paid provider credentials, and can starve authenticated`);
1916
+ console.warn(` remote clients. Not for shared or multi-tenant hosts.`);
1917
+ }
1918
+
1106
1919
  // Prime pool-account quota in the background so the rotation engine has real
1107
1920
  // usage scores from the first routing decision, even when the dashboard is
1108
1921
  // never opened (the common CLI/WSL case). Fire-and-forget: never blocks the
@@ -1114,13 +1927,31 @@ export function startServer(port?: number) {
1114
1927
  && isCanonicalOpenAiForwardProvider(openAiProvider)
1115
1928
  && providerCodexAccountMode("openai", openAiProvider) === "pool"
1116
1929
  ) {
1117
- import("../codex/auth-api")
1930
+ import("../codex/plan-from-token")
1931
+ .then(({ reconcileCodexPlansFromTokens }) => {
1932
+ try {
1933
+ reconcileCodexPlansFromTokens(config);
1934
+ } catch {
1935
+ // Derived plan metadata must not block WHAM priming.
1936
+ }
1937
+ return import("../codex/auth-api");
1938
+ })
1118
1939
  .then(({ primeCodexPoolQuotas }) => primeCodexPoolQuotas(config, "startup"))
1119
1940
  .catch(() => {});
1120
1941
  }
1121
1942
 
1122
1943
  // Opt-in storage policy (default OFF). Never blocks listen; cancellable on shutdown.
1123
- scheduleStorageCleanupStartupRun();
1944
+ backgroundLifecycle.scheduleStartupRun();
1945
+
1946
+ // Compatibility Lab is optional: wire it only for installs that actually use it -- any
1947
+ // routing profile, or automation enabled on disk. This runs synchronously before
1948
+ // startServer returns, in the same turn as Bun.serve, so a policy route can never be
1949
+ // evaluated before its evidence provider is registered. That ordering is load-bearing:
1950
+ // the subagent-fallback chain routes synchronously and has nowhere to await.
1951
+ const labConfigDir = getConfigDir();
1952
+ if (labActivationRequired(config, labConfigDir)) {
1953
+ activateLab(config, labConfigDir);
1954
+ }
1124
1955
 
1125
1956
  return server;
1126
1957
  }