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