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