@remodex/rmx 1.0.2

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