@iislee/opencodex 2.11.0 → 2.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (733) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/README.md +117 -22
  3. package/bin/ocx.mjs +245 -88
  4. package/bin/package-main.mjs +1 -1
  5. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  6. package/gui/dist/assets/index-D4OKgUV4.js +114 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  9. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  10. package/gui/dist/provider-icons/claude-color.svg +1 -1
  11. package/gui/dist/provider-icons/cline-color.svg +16 -0
  12. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  13. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  14. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  15. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  16. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  17. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  18. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  19. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  20. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  21. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  22. package/gui/dist/provider-icons/grok.svg +1 -1
  23. package/gui/dist/provider-icons/groq-color.svg +1 -1
  24. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  25. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  26. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  27. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  28. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  29. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  30. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  31. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  32. package/gui/dist/provider-icons/openai.svg +1 -1
  33. package/gui/dist/provider-icons/opencode.svg +2 -1
  34. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  35. package/gui/dist/provider-icons/pi.svg +2 -2
  36. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  37. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  38. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  39. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  40. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  41. package/package.json +21 -10
  42. package/src/adapters/anthropic-output-schema.ts +137 -0
  43. package/src/adapters/anthropic.ts +475 -62
  44. package/src/adapters/base.ts +82 -7
  45. package/src/adapters/client-fingerprint.ts +18 -12
  46. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  47. package/src/adapters/command-code.ts +637 -0
  48. package/src/adapters/cursor/call-id.ts +44 -0
  49. package/src/adapters/cursor/catalog.ts +541 -0
  50. package/src/adapters/cursor/checkpoint-store.ts +308 -0
  51. package/src/adapters/cursor/cursor-errors.ts +144 -6
  52. package/src/adapters/cursor/discovery.ts +122 -14
  53. package/src/adapters/cursor/effort-map.ts +106 -4
  54. package/src/adapters/cursor/envelope-echo.ts +290 -0
  55. package/src/adapters/cursor/framing.ts +39 -0
  56. package/src/adapters/cursor/h2-pool.ts +123 -0
  57. package/src/adapters/cursor/http1-bidi.ts +361 -0
  58. package/src/adapters/cursor/images.ts +704 -0
  59. package/src/adapters/cursor/live-models.ts +180 -59
  60. package/src/adapters/cursor/live-transport.ts +623 -170
  61. package/src/adapters/cursor/message-mapper.ts +4 -1
  62. package/src/adapters/cursor/native-exec-common.ts +23 -2
  63. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  64. package/src/adapters/cursor/native-exec-fs.ts +10 -7
  65. package/src/adapters/cursor/native-exec-network.ts +1 -1
  66. package/src/adapters/cursor/native-exec-shell.ts +5 -3
  67. package/src/adapters/cursor/native-exec.ts +121 -14
  68. package/src/adapters/cursor/protobuf-events.ts +829 -11
  69. package/src/adapters/cursor/protobuf-request.ts +447 -75
  70. package/src/adapters/cursor/request-builder.ts +271 -35
  71. package/src/adapters/cursor/tool-definitions.ts +252 -12
  72. package/src/adapters/cursor/tool-result-normalize.ts +115 -0
  73. package/src/adapters/cursor/transport.ts +22 -0
  74. package/src/adapters/cursor/types.ts +48 -1
  75. package/src/adapters/cursor.ts +336 -39
  76. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  77. package/src/adapters/google-antigravity-replay.ts +713 -41
  78. package/src/adapters/google-antigravity-wire.ts +48 -10
  79. package/src/adapters/google-errors.ts +44 -12
  80. package/src/adapters/google-http.ts +38 -10
  81. package/src/adapters/google-tool-schema.ts +80 -15
  82. package/src/adapters/google-truncation.ts +11 -0
  83. package/src/adapters/google.ts +742 -86
  84. package/src/adapters/identity.ts +39 -6
  85. package/src/adapters/image.ts +1 -1
  86. package/src/adapters/kiro-constants.ts +27 -0
  87. package/src/adapters/kiro-errors.ts +11 -0
  88. package/src/adapters/kiro-events.ts +19 -1
  89. package/src/adapters/kiro-thinking.ts +18 -2
  90. package/src/adapters/kiro-tools.ts +65 -17
  91. package/src/adapters/kiro.ts +270 -73
  92. package/src/adapters/mimo-free.ts +17 -0
  93. package/src/adapters/openai-chat-url.ts +11 -0
  94. package/src/adapters/openai-chat.ts +1377 -344
  95. package/src/adapters/openai-responses-url.ts +16 -0
  96. package/src/adapters/openai-responses.ts +1164 -64
  97. package/src/adapters/registry.ts +175 -0
  98. package/src/adapters/responses-tool-schema.ts +67 -0
  99. package/src/adapters/run-turn-queue.ts +36 -1
  100. package/src/adapters/tool-call-id.ts +119 -0
  101. package/src/adapters/tool-catalog-nudge.ts +105 -21
  102. package/src/adapters/xai-tool-schema.ts +436 -0
  103. package/src/adapters/xai-web-search.ts +186 -0
  104. package/src/bridge.ts +512 -92
  105. package/src/chat/inbound.ts +57 -20
  106. package/src/chat/outbound.ts +97 -33
  107. package/src/claude/agents-inject.ts +40 -10
  108. package/src/claude/context-windows.ts +37 -14
  109. package/src/claude/desktop-3p.ts +243 -9
  110. package/src/claude/gateway-cache.ts +41 -4
  111. package/src/claude/inbound.ts +72 -3
  112. package/src/claude/model-info.ts +38 -15
  113. package/src/claude/outbound.ts +88 -21
  114. package/src/cli/access.ts +46 -3
  115. package/src/cli/account-api.ts +150 -19
  116. package/src/cli/account-auth.ts +33 -6
  117. package/src/cli/account-catalog-refresh.ts +14 -0
  118. package/src/cli/account-extended.ts +648 -28
  119. package/src/cli/account-main.ts +317 -0
  120. package/src/cli/account.ts +97 -19
  121. package/src/cli/agent.ts +53 -2
  122. package/src/cli/alias.ts +66 -0
  123. package/src/cli/capabilities-command.ts +94 -0
  124. package/src/cli/capabilities.ts +496 -0
  125. package/src/cli/claude-agent-startup-sync.ts +73 -0
  126. package/src/cli/claude-desktop.ts +60 -15
  127. package/src/cli/claude.ts +129 -35
  128. package/src/cli/codex-log-guard-doctor.ts +103 -0
  129. package/src/cli/codex-shim-autorestore.ts +2 -0
  130. package/src/cli/codex-shim-readiness.ts +76 -0
  131. package/src/cli/combo.ts +8 -0
  132. package/src/cli/config-command.ts +74 -10
  133. package/src/cli/dispatch.ts +821 -0
  134. package/src/cli/doctor.ts +440 -56
  135. package/src/cli/ensure-desired-integrations.ts +152 -0
  136. package/src/cli/export-command.ts +46 -20
  137. package/src/cli/help.ts +34 -274
  138. package/src/cli/index.ts +417 -528
  139. package/src/cli/init.ts +5 -17
  140. package/src/cli/inspect.ts +230 -0
  141. package/src/cli/integrations.ts +120 -2
  142. package/src/cli/lab.ts +607 -0
  143. package/src/cli/launcher-context.ts +77 -0
  144. package/src/cli/minimax.ts +497 -0
  145. package/src/cli/models-runtime.ts +130 -2
  146. package/src/cli/models.ts +118 -16
  147. package/src/cli/observe.ts +123 -11
  148. package/src/cli/opencode.ts +4 -2
  149. package/src/cli/provider-runtime.ts +34 -3
  150. package/src/cli/provider.ts +25 -3
  151. package/src/cli/ready.ts +301 -0
  152. package/src/cli/registry.ts +476 -0
  153. package/src/cli/root.ts +86 -0
  154. package/src/cli/route-policy.ts +92 -0
  155. package/src/cli/runtime-api.ts +57 -10
  156. package/src/cli/star-prompt.ts +71 -15
  157. package/src/cli/status.ts +51 -4
  158. package/src/cli/storage.ts +234 -0
  159. package/src/cli/system-command.ts +16 -0
  160. package/src/cli/system-restart-client.ts +146 -0
  161. package/src/cli/tray-proxy.ts +153 -6
  162. package/src/cli/usage-report.ts +184 -0
  163. package/src/cli/v2.ts +137 -18
  164. package/src/cli/version-skew.ts +46 -0
  165. package/src/cli.ts +1 -1
  166. package/src/clients/config-export.ts +1442 -23
  167. package/src/clients/effective-status.ts +30 -2
  168. package/src/clients/probes/cline.ts +81 -0
  169. package/src/codex/account-label.ts +35 -1
  170. package/src/codex/account-lifecycle.ts +130 -13
  171. package/src/codex/account-namespaces.ts +49 -3
  172. package/src/codex/account-pause.ts +2 -1
  173. package/src/codex/account-priority.ts +84 -0
  174. package/src/codex/account-store.ts +29 -2
  175. package/src/codex/account-usability.ts +25 -2
  176. package/src/codex/admission.ts +256 -0
  177. package/src/codex/affinity-debug.ts +162 -0
  178. package/src/codex/app-server-processes.ts +570 -107
  179. package/src/codex/app-server-restart-service.ts +232 -0
  180. package/src/codex/auth-api.ts +893 -246
  181. package/src/codex/auth-collision.ts +5 -3
  182. package/src/codex/auth-context.ts +348 -32
  183. package/src/codex/autostart-health.ts +24 -1
  184. package/src/codex/catalog/account-models.ts +67 -0
  185. package/src/codex/catalog/aggregation.ts +86 -10
  186. package/src/codex/catalog/bundled.ts +331 -33
  187. package/src/codex/catalog/effort.ts +138 -32
  188. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  189. package/src/codex/catalog/kinds.ts +2 -0
  190. package/src/codex/catalog/metadata.ts +555 -45
  191. package/src/codex/catalog/native-models.ts +75 -0
  192. package/src/codex/catalog/parsing.ts +269 -30
  193. package/src/codex/catalog/provider-fetch.ts +1626 -144
  194. package/src/codex/catalog/sync.ts +1576 -187
  195. package/src/codex/catalog-admission.ts +199 -0
  196. package/src/codex/catalog-refresh-status.ts +105 -0
  197. package/src/codex/catalog-write-serialization.ts +242 -0
  198. package/src/codex/catalog.ts +6 -3
  199. package/src/codex/codex-write-lock.ts +386 -0
  200. package/src/codex/convergence-types.ts +614 -0
  201. package/src/codex/convergence.ts +676 -0
  202. package/src/codex/coordinator-doctor.ts +332 -0
  203. package/src/codex/custom-model-catalog-migration.ts +176 -0
  204. package/src/codex/desired-state.ts +230 -0
  205. package/src/codex/desktop-app-restart.ts +355 -0
  206. package/src/codex/features.ts +636 -39
  207. package/src/codex/generation.ts +202 -0
  208. package/src/codex/history-job.ts +436 -0
  209. package/src/codex/history-lock.ts +242 -0
  210. package/src/codex/history-manifest.ts +112 -0
  211. package/src/codex/history-migration-guardian.ts +30 -24
  212. package/src/codex/history-provider.ts +1016 -235
  213. package/src/codex/history-transition.ts +105 -0
  214. package/src/codex/history-worker.ts +223 -0
  215. package/src/codex/inject-coordination.ts +373 -0
  216. package/src/codex/inject.ts +1114 -152
  217. package/src/codex/injected-marker.ts +37 -3
  218. package/src/codex/integration-record.ts +266 -0
  219. package/src/codex/internal/catalog-writer.ts +203 -0
  220. package/src/codex/internal/history-writer.ts +81 -0
  221. package/src/codex/journal.ts +66 -4
  222. package/src/codex/log-guard/inspect.ts +524 -0
  223. package/src/codex/log-guard/lock.ts +150 -0
  224. package/src/codex/log-guard/maintenance.ts +403 -0
  225. package/src/codex/log-guard/path-safety.ts +88 -0
  226. package/src/codex/log-guard/policy.ts +44 -0
  227. package/src/codex/log-guard/processes.ts +205 -0
  228. package/src/codex/log-guard/protection.ts +489 -0
  229. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  230. package/src/codex/main-account-cache.ts +24 -0
  231. package/src/codex/main-account.ts +29 -1
  232. package/src/codex/management-convergence.ts +167 -0
  233. package/src/codex/model-cache.ts +56 -10
  234. package/src/codex/model-entitlements.ts +360 -0
  235. package/src/codex/native-main-admission.ts +47 -0
  236. package/src/codex/native-main-auth-temp.ts +187 -0
  237. package/src/codex/native-main-claim.ts +178 -0
  238. package/src/codex/native-main-lock-file.ts +162 -0
  239. package/src/codex/native-main-owner.ts +329 -0
  240. package/src/codex/native-profile-api.ts +247 -0
  241. package/src/codex/native-profile-manager.ts +1531 -0
  242. package/src/codex/native-profile-processes.ts +121 -0
  243. package/src/codex/native-profile-recovery.ts +99 -0
  244. package/src/codex/native-profile-stage-store.ts +387 -0
  245. package/src/codex/native-profile-startup.ts +622 -0
  246. package/src/codex/native-profile-store.ts +855 -0
  247. package/src/codex/native-profile-types.ts +120 -0
  248. package/src/codex/native-residue.ts +675 -0
  249. package/src/codex/paths.ts +80 -1
  250. package/src/codex/plan-from-token.ts +140 -0
  251. package/src/codex/plan.ts +40 -0
  252. package/src/codex/plugins-doctor.ts +1 -1
  253. package/src/codex/pool-rotation.ts +74 -4
  254. package/src/codex/project-config-warnings.ts +20 -6
  255. package/src/codex/prompt-journal.ts +352 -0
  256. package/src/codex/prompt-layers.ts +1313 -0
  257. package/src/codex/prompt-lock.ts +143 -0
  258. package/src/codex/prompt-text-probe.ts +238 -0
  259. package/src/codex/quota-rejection.ts +298 -0
  260. package/src/codex/quota.ts +264 -21
  261. package/src/codex/refresh.ts +11 -2
  262. package/src/codex/reset-credit-recovery.ts +1044 -0
  263. package/src/codex/routing.ts +514 -94
  264. package/src/codex/runtime.ts +159 -38
  265. package/src/codex/shim.ts +1065 -31
  266. package/src/codex/subagent-model-fallback.ts +437 -43
  267. package/src/codex/sync.ts +191 -2
  268. package/src/codex/transition-state.ts +720 -0
  269. package/src/codex/upstream-host-health.ts +368 -0
  270. package/src/codex/user-identity.ts +557 -0
  271. package/src/codex/warmup.ts +187 -81
  272. package/src/codex/write-coordination.ts +114 -0
  273. package/src/combos/failover.ts +47 -0
  274. package/src/combos/index.ts +4 -0
  275. package/src/combos/request.ts +32 -0
  276. package/src/combos/types.ts +91 -9
  277. package/src/compatibility/index.ts +26 -0
  278. package/src/compatibility/manifest.ts +253 -0
  279. package/src/compatibility/openai-responses.ts +121 -0
  280. package/src/config/atomic-write.ts +219 -0
  281. package/src/config/paths.ts +40 -0
  282. package/src/config/process-state.ts +309 -0
  283. package/src/config/provider-name.ts +24 -0
  284. package/src/config/provider-validation.ts +177 -0
  285. package/src/config/rebase-provenance.ts +68 -0
  286. package/src/config.ts +1713 -709
  287. package/src/generated/compatibility-version.json +3324 -0
  288. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
  289. package/src/grok/inject.ts +16 -5
  290. package/src/grok/inspect.ts +45 -0
  291. package/src/grok/sync.ts +2 -2
  292. package/src/images/loop.ts +157 -33
  293. package/src/images/plan.ts +24 -13
  294. package/src/integrations/config-io.ts +269 -0
  295. package/src/integrations/journal.ts +315 -0
  296. package/src/integrations/merge.ts +135 -0
  297. package/src/integrations/mutation-flight.ts +71 -0
  298. package/src/integrations/native/ownership-preflight.ts +202 -0
  299. package/src/integrations/omp-yaml-source.ts +358 -0
  300. package/src/integrations/owned-refresh.ts +74 -0
  301. package/src/integrations/ownership-policy.ts +160 -0
  302. package/src/integrations/ownership.ts +155 -0
  303. package/src/integrations/registry.ts +166 -0
  304. package/src/integrations/serialize.ts +314 -0
  305. package/src/integrations/state.ts +433 -0
  306. package/src/integrations/store.ts +103 -0
  307. package/src/integrations/writer-lock.ts +98 -0
  308. package/src/integrations/writer.ts +715 -0
  309. package/src/lab/artifacts/sanitize.ts +586 -0
  310. package/src/lab/artifacts/secure-fs.ts +475 -0
  311. package/src/lab/artifacts/store.ts +310 -0
  312. package/src/lab/automation/budgets.ts +78 -0
  313. package/src/lab/automation/config-persistence.ts +256 -0
  314. package/src/lab/automation/constants.ts +39 -0
  315. package/src/lab/automation/cooldown.ts +103 -0
  316. package/src/lab/automation/dispatch.ts +211 -0
  317. package/src/lab/automation/index.ts +13 -0
  318. package/src/lab/automation/orchestrator.ts +499 -0
  319. package/src/lab/automation/persistence.ts +512 -0
  320. package/src/lab/automation/planner.ts +371 -0
  321. package/src/lab/automation/policy.ts +136 -0
  322. package/src/lab/automation/queue.ts +191 -0
  323. package/src/lab/automation/recovery.ts +24 -0
  324. package/src/lab/automation/route-context.ts +21 -0
  325. package/src/lab/automation/run-key.ts +44 -0
  326. package/src/lab/automation/runs-query.ts +34 -0
  327. package/src/lab/automation/types.ts +160 -0
  328. package/src/lab/conformance/assertion.ts +325 -0
  329. package/src/lab/conformance/digest.ts +22 -0
  330. package/src/lab/conformance/executor.ts +741 -0
  331. package/src/lab/conformance/fixture-provider.ts +27 -0
  332. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  333. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  334. package/src/lab/conformance/harness-budget.ts +47 -0
  335. package/src/lab/conformance/index.ts +5 -0
  336. package/src/lab/conformance/jcs.ts +64 -0
  337. package/src/lab/conformance/json-pointer.ts +39 -0
  338. package/src/lab/conformance/manifest.ts +180 -0
  339. package/src/lab/conformance/mcp-stub.ts +179 -0
  340. package/src/lab/conformance/negative-controls.ts +164 -0
  341. package/src/lab/conformance/observation.ts +355 -0
  342. package/src/lab/conformance/runner.ts +68 -0
  343. package/src/lab/conformance/sse-normalize.ts +59 -0
  344. package/src/lab/conformance/suite-manifest.ts +78 -0
  345. package/src/lab/conformance/types.ts +214 -0
  346. package/src/lab/constants.ts +126 -0
  347. package/src/lab/digest.ts +64 -0
  348. package/src/lab/events/errors.ts +9 -0
  349. package/src/lab/events/limits.ts +117 -0
  350. package/src/lab/events/types.ts +229 -0
  351. package/src/lab/events/validate.ts +781 -0
  352. package/src/lab/fabric/constants.ts +40 -0
  353. package/src/lab/fabric/executor.ts +492 -0
  354. package/src/lab/fabric/index.ts +80 -0
  355. package/src/lab/fabric/manifest.ts +222 -0
  356. package/src/lab/fabric/observe.ts +489 -0
  357. package/src/lab/fabric/patch.ts +79 -0
  358. package/src/lab/fabric/producer-child.ts +139 -0
  359. package/src/lab/fabric/producer-isolate.ts +276 -0
  360. package/src/lab/fabric/producer-protocol.ts +61 -0
  361. package/src/lab/fabric/scratch.ts +439 -0
  362. package/src/lab/fabric/subject.ts +106 -0
  363. package/src/lab/fabric/types.ts +134 -0
  364. package/src/lab/fabric/verifier.ts +98 -0
  365. package/src/lab/index.ts +54 -0
  366. package/src/lab/ledger/artifact-refs.ts +127 -0
  367. package/src/lab/ledger/invalidation.ts +136 -0
  368. package/src/lab/ledger/purge.ts +310 -0
  369. package/src/lab/ledger/store.ts +532 -0
  370. package/src/lab/live/credential-lease.ts +53 -0
  371. package/src/lab/live/destination.ts +155 -0
  372. package/src/lab/live/executor.ts +336 -0
  373. package/src/lab/live/inert-tools.ts +56 -0
  374. package/src/lab/live/manifest.ts +85 -0
  375. package/src/lab/live/mcp-loopback.ts +57 -0
  376. package/src/lab/live/runner.ts +19 -0
  377. package/src/lab/live/sandbox.ts +61 -0
  378. package/src/lab/live/suite-manifest.ts +41 -0
  379. package/src/lab/live/transport.ts +118 -0
  380. package/src/lab/live/types.ts +197 -0
  381. package/src/lab/observe/from-conformance.ts +301 -0
  382. package/src/lab/observe/from-live.ts +117 -0
  383. package/src/lab/paths.ts +153 -0
  384. package/src/lab/projection/rebuild.ts +495 -0
  385. package/src/lab/projection/schema.ts +135 -0
  386. package/src/lab/projection/verdicts.ts +474 -0
  387. package/src/lab/projection/verification.ts +412 -0
  388. package/src/lab/public/bundle.ts +217 -0
  389. package/src/lab/public/community-authority.ts +175 -0
  390. package/src/lab/public/community-files.ts +29 -0
  391. package/src/lab/public/community.ts +479 -0
  392. package/src/lab/public/file-safety.ts +155 -0
  393. package/src/lab/public/ids.ts +26 -0
  394. package/src/lab/public/index.ts +16 -0
  395. package/src/lab/public/mutation-lock.ts +424 -0
  396. package/src/lab/public/operator.ts +353 -0
  397. package/src/lab/public/origin-purge.ts +79 -0
  398. package/src/lab/public/origin.ts +203 -0
  399. package/src/lab/public/privacy.ts +143 -0
  400. package/src/lab/public/private-file.ts +261 -0
  401. package/src/lab/public/project.ts +124 -0
  402. package/src/lab/public/purge-test-fault.ts +21 -0
  403. package/src/lab/public/purge.ts +223 -0
  404. package/src/lab/public/registry.ts +44 -0
  405. package/src/lab/public/revocation.ts +252 -0
  406. package/src/lab/public/signature.ts +243 -0
  407. package/src/lab/public/storage.ts +105 -0
  408. package/src/lab/public/strict-json.ts +206 -0
  409. package/src/lab/public/time.ts +26 -0
  410. package/src/lab/public/types.ts +172 -0
  411. package/src/lab/public/validate.ts +391 -0
  412. package/src/lab/query/catalog.ts +101 -0
  413. package/src/lab/query/connection.ts +107 -0
  414. package/src/lab/query/constants.ts +4 -0
  415. package/src/lab/query/cursor.ts +132 -0
  416. package/src/lab/query/dto-map.ts +277 -0
  417. package/src/lab/query/errors.ts +22 -0
  418. package/src/lab/query/freshness.ts +53 -0
  419. package/src/lab/query/index.ts +45 -0
  420. package/src/lab/query/latest-observation.ts +59 -0
  421. package/src/lab/query/passive-production.ts +159 -0
  422. package/src/lab/query/queries.ts +444 -0
  423. package/src/lab/query/types.ts +266 -0
  424. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  425. package/src/lab/subject/installation-salt.ts +112 -0
  426. package/src/lab/subject/protocol-subject.ts +80 -0
  427. package/src/lab/subject/route-subject.ts +74 -0
  428. package/src/lib/admin-secrets.ts +24 -0
  429. package/src/lib/app-owned-memory-stores.ts +22 -0
  430. package/src/lib/bounded-body.ts +167 -11
  431. package/src/lib/bun-runtime.ts +125 -12
  432. package/src/lib/bun-stream-caps.ts +13 -9
  433. package/src/lib/codex-restart-contract.ts +120 -0
  434. package/src/lib/config-ownership.ts +26 -2
  435. package/src/lib/destination-policy.ts +65 -1
  436. package/src/lib/errors.ts +80 -5
  437. package/src/lib/fabric-task-execution-authority.ts +7 -0
  438. package/src/lib/fabric-task-host.ts +29 -0
  439. package/src/lib/lab-activation.ts +223 -0
  440. package/src/lib/lab-live-execution-authority.ts +13 -0
  441. package/src/lib/lab-live-host.ts +30 -0
  442. package/src/lib/lab-live-pinned-sender.ts +56 -0
  443. package/src/lib/lab-live-route-production.ts +130 -0
  444. package/src/lib/lab-passive-linker-registration.ts +26 -0
  445. package/src/lib/local-management-attestation.ts +51 -0
  446. package/src/lib/local-management-capability.ts +100 -0
  447. package/src/lib/local-provider-reload-contract.ts +100 -0
  448. package/src/lib/optional-shutdown-hooks.ts +57 -0
  449. package/src/lib/package-tree-integrity.ts +101 -0
  450. package/src/lib/pinned-http.ts +145 -26
  451. package/src/lib/process-control.ts +6 -2
  452. package/src/lib/provider-outbound.ts +49 -9
  453. package/src/lib/redact.ts +419 -3
  454. package/src/lib/self-launch-argv.ts +15 -0
  455. package/src/lib/server-resource-ownership.ts +71 -0
  456. package/src/lib/service-secrets.ts +15 -0
  457. package/src/lib/shadow-call.ts +35 -4
  458. package/src/lib/sse-decoder.ts +41 -0
  459. package/src/lib/state-store-registrations.ts +10 -2
  460. package/src/lib/system-restart-contract.ts +73 -0
  461. package/src/lib/token-estimate.ts +19 -2
  462. package/src/lib/tool-argument-integers.ts +253 -0
  463. package/src/lib/translator-budget.ts +44 -0
  464. package/src/lib/upstream-http-version.ts +57 -0
  465. package/src/lib/upstream-reachability.ts +95 -0
  466. package/src/lib/upstream-retry.ts +156 -3
  467. package/src/lib/windows-atomic-replace.ts +155 -0
  468. package/src/lib/windows-elevation.ts +70 -2
  469. package/src/lib/windows-secret-acl.ts +409 -69
  470. package/src/lib/windows-service-wrappers.ts +72 -0
  471. package/src/lib/windows-text.ts +106 -0
  472. package/src/lib/windows-user-principal.ts +341 -0
  473. package/src/lib/winsw.ts +33 -5
  474. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  475. package/src/oauth/account-import/index.ts +15 -0
  476. package/src/oauth/account-import/parser.ts +83 -0
  477. package/src/oauth/account-import/registry.ts +18 -0
  478. package/src/oauth/account-import/service.ts +75 -0
  479. package/src/oauth/account-import/types.ts +91 -0
  480. package/src/oauth/anthropic.ts +12 -1
  481. package/src/oauth/callback-server.ts +30 -4
  482. package/src/oauth/chatgpt.ts +12 -1
  483. package/src/oauth/cline.ts +203 -0
  484. package/src/oauth/command-code.ts +239 -0
  485. package/src/oauth/cursor.ts +46 -5
  486. package/src/oauth/generic-account-failover.ts +231 -0
  487. package/src/oauth/google-antigravity.ts +35 -3
  488. package/src/oauth/health.ts +20 -12
  489. package/src/oauth/index.ts +462 -71
  490. package/src/oauth/key-providers.ts +25 -0
  491. package/src/oauth/kimi.ts +25 -3
  492. package/src/oauth/kiro.ts +95 -6
  493. package/src/oauth/local-token-detect.ts +13 -2
  494. package/src/oauth/log.ts +3 -1
  495. package/src/oauth/login-cli.ts +88 -28
  496. package/src/oauth/nous.ts +798 -0
  497. package/src/oauth/open-browser-choice.ts +26 -0
  498. package/src/oauth/store.ts +133 -26
  499. package/src/oauth/token-guardian.ts +9 -3
  500. package/src/oauth/types.ts +15 -0
  501. package/src/pi/models.ts +2 -2
  502. package/src/providers/alibaba-region-migration.ts +1 -1
  503. package/src/providers/antigravity-models.ts +521 -31
  504. package/src/providers/auto-compact-budget.ts +65 -0
  505. package/src/providers/base-url-choices.ts +10 -0
  506. package/src/providers/codex-capacity.ts +292 -0
  507. package/src/providers/command-code-efforts.ts +176 -0
  508. package/src/providers/context-cap.ts +26 -8
  509. package/src/providers/cursor-pool.ts +72 -0
  510. package/src/providers/default-aliases.ts +65 -0
  511. package/src/providers/derive.ts +281 -6
  512. package/src/providers/fastwire.ts +507 -0
  513. package/src/providers/free-directory.ts +10 -7
  514. package/src/providers/google-vertex-location.ts +14 -0
  515. package/src/providers/key-failover.ts +71 -3
  516. package/src/providers/label.ts +35 -2
  517. package/src/providers/model-discovery-limits.ts +16 -0
  518. package/src/providers/model-discovery.ts +115 -22
  519. package/src/providers/model-presets.ts +119 -0
  520. package/src/providers/model-rename-migration.ts +255 -0
  521. package/src/providers/model-rename-startup.ts +28 -0
  522. package/src/providers/new-model-policy.ts +146 -0
  523. package/src/providers/openai-sidecar.ts +72 -4
  524. package/src/providers/openai-tier-startup.ts +31 -2
  525. package/src/providers/openai-tiers.ts +119 -4
  526. package/src/providers/openai-virtual-models.ts +1 -0
  527. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  528. package/src/providers/provider-id-rewrite.ts +30 -0
  529. package/src/providers/quota.ts +1379 -40
  530. package/src/providers/registry.ts +1545 -112
  531. package/src/providers/request-pacing.ts +310 -0
  532. package/src/providers/service-tier.ts +277 -0
  533. package/src/providers/slug-codec.ts +94 -6
  534. package/src/providers/static-model-discovery.ts +86 -0
  535. package/src/providers/xai-responses-opt-in.ts +15 -0
  536. package/src/providers/xai-transport.ts +32 -4
  537. package/src/reasoning-effort.ts +68 -3
  538. package/src/responses/apply-patch-envelope.ts +63 -0
  539. package/src/responses/code-mode-helper-compat.ts +50 -0
  540. package/src/responses/compaction.ts +26 -1
  541. package/src/responses/custom-tool-compat.ts +384 -0
  542. package/src/responses/hosted-tool-policy.ts +9 -0
  543. package/src/responses/namespace-tool-compat.ts +435 -0
  544. package/src/responses/parser.ts +225 -38
  545. package/src/responses/provider-continuation.ts +98 -0
  546. package/src/responses/provider-opaque-metadata.ts +73 -0
  547. package/src/responses/reasoning-envelope.ts +9 -1
  548. package/src/responses/reasoning-replay-cache.ts +504 -0
  549. package/src/responses/schema.ts +12 -2
  550. package/src/responses/spill-store.ts +75 -10
  551. package/src/responses/state.ts +659 -31
  552. package/src/responses/thought-signature-replay.ts +364 -0
  553. package/src/responses/tool-search-compat.ts +301 -0
  554. package/src/responses/truncated-stop-reason.ts +60 -0
  555. package/src/responses/turn-termination.ts +107 -0
  556. package/src/router.ts +405 -31
  557. package/src/routing/analytics.ts +379 -0
  558. package/src/routing/capability.ts +244 -0
  559. package/src/routing/compatibility/assemble.ts +73 -0
  560. package/src/routing/compatibility/behavior.ts +278 -0
  561. package/src/routing/compatibility/catalog.ts +99 -0
  562. package/src/routing/compatibility/endpoint.ts +52 -0
  563. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  564. package/src/routing/compatibility/policy.ts +181 -0
  565. package/src/routing/compatibility/provider-slot.ts +56 -0
  566. package/src/routing/compatibility/reader.ts +110 -0
  567. package/src/routing/compatibility/subject.ts +191 -0
  568. package/src/routing/compatibility/types.ts +64 -0
  569. package/src/routing/compatibility/version.ts +104 -0
  570. package/src/routing/cost.ts +77 -0
  571. package/src/routing/evaluator.ts +495 -0
  572. package/src/routing/health.ts +412 -0
  573. package/src/routing/history/cursor.ts +43 -0
  574. package/src/routing/history/indexer.ts +605 -0
  575. package/src/routing/history/schema.ts +72 -0
  576. package/src/routing/profile-namespace.ts +15 -0
  577. package/src/routing/profile.ts +547 -0
  578. package/src/routing/quota.ts +155 -0
  579. package/src/routing/request-evidence.ts +45 -0
  580. package/src/routing/trace.ts +776 -0
  581. package/src/server/adapter-resolve.ts +2 -29
  582. package/src/server/auth-cors.ts +289 -48
  583. package/src/server/background-lifecycle.ts +182 -0
  584. package/src/server/chat-completions.ts +154 -72
  585. package/src/server/chat-native-sse.ts +331 -0
  586. package/src/server/chat-native.ts +445 -0
  587. package/src/server/claude-messages.ts +160 -44
  588. package/src/server/direct-local-http.ts +347 -0
  589. package/src/server/effort-policy.ts +18 -0
  590. package/src/server/github-copilot-responses-repair.ts +338 -0
  591. package/src/server/gui-static.ts +39 -10
  592. package/src/server/images.ts +94 -12
  593. package/src/server/index.ts +1021 -190
  594. package/src/server/lifecycle.ts +336 -14
  595. package/src/server/live.ts +136 -17
  596. package/src/server/local-management-read-client.ts +90 -0
  597. package/src/server/local-provider-reload-client.ts +137 -0
  598. package/src/server/management/agent-settings-routes.ts +433 -116
  599. package/src/server/management/api-key-usage.ts +31 -5
  600. package/src/server/management/body.ts +6 -0
  601. package/src/server/management/codex-prompt-routes.ts +570 -0
  602. package/src/server/management/combo-routes.ts +64 -25
  603. package/src/server/management/config-routes.ts +511 -51
  604. package/src/server/management/context.ts +89 -2
  605. package/src/server/management/integration-routes.ts +498 -0
  606. package/src/server/management/lab-automation-routes.ts +206 -0
  607. package/src/server/management/lab-routes.ts +563 -0
  608. package/src/server/management/logs-usage-routes.ts +127 -52
  609. package/src/server/management/model-routes.ts +461 -133
  610. package/src/server/management/model-rows.ts +167 -0
  611. package/src/server/management/native-integration-routes.ts +770 -0
  612. package/src/server/management/oauth-account-routes.ts +90 -8
  613. package/src/server/management/provider-capability-config.ts +48 -0
  614. package/src/server/management/provider-routes.ts +836 -159
  615. package/src/server/management/request-history-routes.ts +191 -0
  616. package/src/server/management/route-registry.ts +332 -0
  617. package/src/server/management/routing-analytics-routes.ts +74 -0
  618. package/src/server/management/routing-profile-routes.ts +380 -0
  619. package/src/server/management/shared.ts +27 -11
  620. package/src/server/management/sidebar-routes.ts +47 -31
  621. package/src/server/management/storage-log-guard-routes.ts +186 -0
  622. package/src/server/management/sync-response.ts +69 -0
  623. package/src/server/management/system-restart.ts +289 -33
  624. package/src/server/management/system-routes.ts +77 -0
  625. package/src/server/management/usage-summary-cache.ts +9 -1
  626. package/src/server/management/vision-sidecar-options.ts +167 -0
  627. package/src/server/management/web-search-sidecar-options.ts +120 -0
  628. package/src/server/management-api.ts +126 -23
  629. package/src/server/management-auth.ts +220 -5
  630. package/src/server/passive-route-linker.ts +66 -0
  631. package/src/server/port-reclaim.ts +1 -1
  632. package/src/server/ports.ts +41 -1
  633. package/src/server/proxy-liveness.ts +161 -10
  634. package/src/server/readiness.ts +99 -0
  635. package/src/server/relay-eager.ts +221 -54
  636. package/src/server/relay.ts +479 -87
  637. package/src/server/request-decompress.ts +113 -6
  638. package/src/server/request-log-conversation.ts +51 -0
  639. package/src/server/request-log.ts +298 -31
  640. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  641. package/src/server/responses/agent-task-recovery.ts +465 -0
  642. package/src/server/responses/codex-auth-error.ts +55 -0
  643. package/src/server/responses/collaboration.ts +239 -36
  644. package/src/server/responses/combo-stream-preflight.ts +178 -0
  645. package/src/server/responses/compact.ts +444 -72
  646. package/src/server/responses/core.ts +3579 -436
  647. package/src/server/responses/empty-completion-guard.ts +333 -0
  648. package/src/server/responses/encrypted-payload.ts +62 -39
  649. package/src/server/responses/fetch-helpers.ts +89 -99
  650. package/src/server/responses/input-admission.ts +187 -0
  651. package/src/server/responses/pacing-overload.ts +13 -0
  652. package/src/server/responses/passthrough-error.ts +33 -9
  653. package/src/server/responses/policy-fallback.ts +178 -0
  654. package/src/server/responses/responses-field-backfill.ts +343 -0
  655. package/src/server/responses/terminal-guard.ts +26 -5
  656. package/src/server/responses/upstream-error.ts +5 -0
  657. package/src/server/responses/ws-upstream.ts +381 -0
  658. package/src/server/responses-custom-tool-repair.ts +363 -0
  659. package/src/server/responses-item-id-repair.ts +54 -6
  660. package/src/server/responses-json-events.ts +90 -0
  661. package/src/server/responses-model-rewrite.ts +29 -0
  662. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  663. package/src/server/responses-snapshot-repair.ts +621 -0
  664. package/src/server/responses-terminal-repair.ts +363 -0
  665. package/src/server/responses-tool-search-repair.ts +267 -0
  666. package/src/server/responses-undeclared-tool-guard.ts +376 -0
  667. package/src/server/responses.ts +18 -2
  668. package/src/server/search.ts +78 -13
  669. package/src/server/sse-frame-buffer.ts +319 -0
  670. package/src/server/sse-payload-rewrite.ts +110 -22
  671. package/src/server/startup-action-control.ts +8 -1
  672. package/src/server/startup-health-cache.ts +19 -1
  673. package/src/server/system-env.ts +80 -9
  674. package/src/server/ws-bridge.ts +53 -40
  675. package/src/service-manager-probe.ts +892 -0
  676. package/src/service.ts +1153 -94
  677. package/src/sidecar/auth.ts +92 -0
  678. package/src/sidecar/candidates.ts +83 -0
  679. package/src/storage/cleanup.ts +2 -2
  680. package/src/storage/policy-job.ts +14 -4
  681. package/src/storage/policy.ts +88 -23
  682. package/src/storage/scanner.ts +1 -1
  683. package/src/storage/worker-lifecycle.ts +14 -14
  684. package/src/tray/windows-tray.ps1 +83 -9
  685. package/src/tray/windows.ts +43 -16
  686. package/src/types/accounts.ts +37 -0
  687. package/src/types/config.ts +911 -0
  688. package/src/types/provider.ts +614 -0
  689. package/src/types/request.ts +397 -0
  690. package/src/types/tools.ts +236 -0
  691. package/src/types/wire.ts +80 -0
  692. package/src/types.ts +107 -1237
  693. package/src/update/index.ts +37 -23
  694. package/src/update/job.ts +445 -68
  695. package/src/update/notify.ts +12 -6
  696. package/src/update/npm-cache-preflight.d.mts +47 -0
  697. package/src/update/npm-cache-preflight.mjs +201 -0
  698. package/src/update/transactional-install.d.mts +22 -0
  699. package/src/update/transactional-install.mjs +266 -0
  700. package/src/usage/cost.ts +0 -0
  701. package/src/usage/expected-prices.ts +287 -16
  702. package/src/usage/log.ts +666 -50
  703. package/src/usage/summary.ts +388 -17
  704. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  705. package/src/usage/user-cost-overlays.ts +314 -0
  706. package/src/vision/anthropic-describe.ts +10 -6
  707. package/src/vision/backends.ts +97 -0
  708. package/src/vision/describe.ts +27 -16
  709. package/src/vision/eligibility.ts +250 -0
  710. package/src/vision/index.ts +238 -24
  711. package/src/vision/reasoning.ts +55 -0
  712. package/src/vision/routed-describe.ts +175 -0
  713. package/src/vision/timeout-bounds.ts +9 -0
  714. package/src/web-search/anthropic-executor.ts +13 -7
  715. package/src/web-search/backends.ts +108 -0
  716. package/src/web-search/exa-executor.ts +119 -0
  717. package/src/web-search/executor.ts +21 -6
  718. package/src/web-search/gemini-executor.ts +141 -0
  719. package/src/web-search/index.ts +150 -15
  720. package/src/web-search/loop.ts +284 -54
  721. package/src/web-search/parse.ts +125 -30
  722. package/src/web-search/sources.ts +60 -0
  723. package/src/web-search/xai-executor.ts +219 -0
  724. package/gui/dist/assets/index-DTpMHS4F.js +0 -67
  725. package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
  726. package/gui/dist/provider-icons/antigravity.svg +0 -1
  727. package/gui/dist/provider-icons/claude.svg +0 -1
  728. package/gui/dist/provider-icons/copilot.svg +0 -1
  729. package/gui/dist/provider-icons/cursor.svg +0 -2
  730. package/gui/dist/provider-icons/gemini.svg +0 -1
  731. package/gui/dist/provider-icons/grok-color.svg +0 -1
  732. package/gui/dist/provider-icons/kiro.svg +0 -14
  733. package/src/cli/internal-dispatch.ts +0 -20
@@ -13,7 +13,33 @@ import {
13
13
  } from "node:fs";
14
14
  import { dirname, join } from "node:path";
15
15
  import { adminApiTokenFilePath } from "../lib/admin-secrets";
16
- import { forgetHardenedSecretPath, hardenSecretDir, hardenSecretPath } from "../lib/windows-secret-acl";
16
+ import {
17
+ LOCAL_MANAGEMENT_CAPABILITY_HEADER,
18
+ LOCAL_MANAGEMENT_CAPABILITY_EXPIRES_AT_HEADER,
19
+ LOCAL_MANAGEMENT_EXPECTED_PID_HEADER,
20
+ LOCAL_MANAGEMENT_NONCE_HEADER,
21
+ parseExpectedLocalManagementPid,
22
+ verifyLocalManagementReadCapability,
23
+ } from "../lib/local-management-capability";
24
+ import {
25
+ SYSTEM_RESTART_CAPABILITY_HEADER,
26
+ SYSTEM_RESTART_EXPECTED_PID_HEADER,
27
+ SYSTEM_RESTART_NONCE_HEADER,
28
+ SYSTEM_RESTART_PATH,
29
+ parseExpectedSystemRestartPid,
30
+ verifySystemRestartCapability,
31
+ } from "../lib/system-restart-contract";
32
+ import {
33
+ LOCAL_PROVIDER_RELOAD_CAPABILITY_HEADER,
34
+ LOCAL_PROVIDER_RELOAD_EXPECTED_PID_HEADER,
35
+ LOCAL_PROVIDER_RELOAD_EXPIRES_AT_HEADER,
36
+ LOCAL_PROVIDER_RELOAD_NAME_HEADER,
37
+ LOCAL_PROVIDER_RELOAD_NONCE_HEADER,
38
+ LOCAL_PROVIDER_RELOAD_PATH,
39
+ parseExpectedLocalProviderReloadPid,
40
+ verifyLocalProviderReloadCapability,
41
+ } from "../lib/local-provider-reload-contract";
42
+ import { forgetEphemeralSecretPath, forgetHardenedSecretPath, hardenSecretDir, hardenSecretPath } from "../lib/windows-secret-acl";
17
43
  import type { OcxConfig } from "../types";
18
44
  import {
19
45
  isAllowedManagementOrigin,
@@ -26,6 +52,12 @@ import {
26
52
 
27
53
  const GUI_SESSION_TTL_MS = 5 * 60_000;
28
54
  const GUI_SESSION_LIMIT = 128;
55
+ const LOCAL_READ_REPLAY_LIMIT = 256;
56
+ const consumedLocalReadCapabilities = new Map<string, number>();
57
+ const admittedLocalReadRequests = new WeakSet<Request>();
58
+ const LOCAL_PROVIDER_RELOAD_REPLAY_LIMIT = 256;
59
+ const consumedLocalProviderReloadCapabilities = new Map<string, number>();
60
+ const admittedLocalProviderReloadRequests = new WeakSet<Request>();
29
61
 
30
62
  interface GuiSessionRecord {
31
63
  csrfToken: string;
@@ -95,12 +127,17 @@ function readExistingToken(path: string): string {
95
127
  export function removeManagementTokenPathBestEffort(
96
128
  path: string,
97
129
  remove: (path: string) => void = unlinkSync,
130
+ options?: { ephemeral?: boolean },
98
131
  ): void {
132
+ // Temps get the full ephemeral release (success + both timeout namespaces);
133
+ // stable token paths drop only the success memo — destination-keyed timeout
134
+ // memos are intentional anti-restall state.
135
+ const forget = options?.ephemeral ? forgetEphemeralSecretPath : forgetHardenedSecretPath;
99
136
  try {
100
137
  remove(path);
101
- forgetHardenedSecretPath(path);
138
+ forget(path);
102
139
  } catch (error) {
103
- if ((error as NodeJS.ErrnoException).code === "ENOENT") forgetHardenedSecretPath(path);
140
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") forget(path);
104
141
  /* other failures retain fail-closed state for the caller */
105
142
  }
106
143
  }
@@ -120,7 +157,8 @@ function createTokenFile(path: string): string {
120
157
  chmodSync(temporary, 0o600);
121
158
  let temporaryHardened: { ok: boolean };
122
159
  try {
123
- temporaryHardened = hardenSecretPath(temporary, { required: true });
160
+ // Destination-keyed timeout memo (the final token path), not the temp.
161
+ temporaryHardened = hardenSecretPath(temporary, { required: true, timeoutMemoKey: path });
124
162
  } catch {
125
163
  temporaryHardened = { ok: false };
126
164
  }
@@ -155,7 +193,7 @@ function createTokenFile(path: string): string {
155
193
  if (fd !== null) {
156
194
  try { closeSync(fd); } catch { /* best effort */ }
157
195
  }
158
- removeManagementTokenPathBestEffort(temporary);
196
+ removeManagementTokenPathBestEffort(temporary, unlinkSync, { ephemeral: true });
159
197
  }
160
198
  }
161
199
 
@@ -231,11 +269,188 @@ export function issueGuiSession(
231
269
  return { token, ...session };
232
270
  }
233
271
 
272
+ /**
273
+ * Which credential actually authorized a management request.
274
+ *
275
+ * `admin-token` is the raw token from disk/env: anything running as the user can
276
+ * read it, including a coding agent. `gui-session` is a session token this process
277
+ * minted for a browser, and it only authorizes a mutation after the origin and the
278
+ * per-session CSRF token match. Consent-bearing routes must key off this value
279
+ * rather than off request headers, which the token holder can forge freely.
280
+ * The capability principals are process-scoped HMACs bound to the current process
281
+ * PID and listening port. Local reads are accepted only for two exact GET paths;
282
+ * restart and provider reload remain separate wire contracts for their exact POSTs.
283
+ */
284
+ export type ManagementPrincipal =
285
+ | "admin-token"
286
+ | "gui-session"
287
+ | "local-read-capability"
288
+ | "local-provider-reload-capability"
289
+ | "system-restart-capability";
290
+
291
+ export interface LocalManagementAuthContext {
292
+ attestationSecret: string;
293
+ pid: number;
294
+ port: number;
295
+ }
296
+
297
+ function hasSystemRestartCapability(
298
+ req: Request,
299
+ local: LocalManagementAuthContext | undefined,
300
+ ): boolean {
301
+ if (!local || req.method !== "POST") return false;
302
+ let path: string;
303
+ try {
304
+ path = new URL(req.url).pathname;
305
+ } catch {
306
+ return false;
307
+ }
308
+ if (path !== SYSTEM_RESTART_PATH) return false;
309
+ const expectedPid = parseExpectedSystemRestartPid(
310
+ req.headers.get(SYSTEM_RESTART_EXPECTED_PID_HEADER),
311
+ );
312
+ if (expectedPid.kind !== "present" || expectedPid.pid !== local.pid) return false;
313
+ return verifySystemRestartCapability(
314
+ local.attestationSecret,
315
+ req.headers.get(SYSTEM_RESTART_NONCE_HEADER),
316
+ req.method,
317
+ path,
318
+ local.pid,
319
+ local.port,
320
+ req.headers.get(SYSTEM_RESTART_CAPABILITY_HEADER),
321
+ );
322
+ }
323
+
324
+ function hasLocalReadCapability(
325
+ req: Request,
326
+ local: LocalManagementAuthContext | undefined,
327
+ ): boolean {
328
+ // requireManagementAuth and managementPrincipal inspect the same Request in
329
+ // sequence. Preserve that one admission without accepting a replayed request.
330
+ if (admittedLocalReadRequests.has(req)) return true;
331
+ if (!local || req.method !== "GET") return false;
332
+ let url: URL;
333
+ try {
334
+ url = new URL(req.url);
335
+ } catch {
336
+ return false;
337
+ }
338
+ // Do not let a future query-bearing variant silently inherit this narrow grant.
339
+ if (url.search !== "") return false;
340
+ const expectedPid = parseExpectedLocalManagementPid(
341
+ req.headers.get(LOCAL_MANAGEMENT_EXPECTED_PID_HEADER),
342
+ );
343
+ if (expectedPid.kind !== "present" || expectedPid.pid !== local.pid) return false;
344
+ const expiresAtRaw = req.headers.get(LOCAL_MANAGEMENT_CAPABILITY_EXPIRES_AT_HEADER);
345
+ if (!expiresAtRaw || !/^[1-9]\d*$/.test(expiresAtRaw)) return false;
346
+ const expiresAt = Number(expiresAtRaw);
347
+ if (!Number.isSafeInteger(expiresAt)) return false;
348
+ const capability = req.headers.get(LOCAL_MANAGEMENT_CAPABILITY_HEADER);
349
+ const now = Date.now();
350
+ if (!verifyLocalManagementReadCapability(
351
+ local.attestationSecret,
352
+ req.headers.get(LOCAL_MANAGEMENT_NONCE_HEADER),
353
+ req.method,
354
+ url.pathname,
355
+ local.pid,
356
+ local.port,
357
+ expiresAt,
358
+ capability,
359
+ now,
360
+ )) return false;
361
+ for (const [consumed, retainedUntil] of consumedLocalReadCapabilities) {
362
+ if (retainedUntil <= now) consumedLocalReadCapabilities.delete(consumed);
363
+ }
364
+ if (!capability || consumedLocalReadCapabilities.has(capability)) return false;
365
+ if (consumedLocalReadCapabilities.size >= LOCAL_READ_REPLAY_LIMIT) return false;
366
+ consumedLocalReadCapabilities.set(capability, expiresAt);
367
+ admittedLocalReadRequests.add(req);
368
+ return true;
369
+ }
370
+
371
+ function hasLocalProviderReloadCapability(
372
+ req: Request,
373
+ local: LocalManagementAuthContext | undefined,
374
+ ): boolean {
375
+ if (admittedLocalProviderReloadRequests.has(req)) return true;
376
+ if (!local || req.method !== "POST") return false;
377
+ let url: URL;
378
+ try {
379
+ url = new URL(req.url);
380
+ } catch {
381
+ return false;
382
+ }
383
+ if (url.pathname !== LOCAL_PROVIDER_RELOAD_PATH || url.search !== "") return false;
384
+ const contentLength = req.headers.get("content-length");
385
+ if (contentLength !== "0" || req.headers.has("transfer-encoding")) return false;
386
+ const expectedPid = parseExpectedLocalProviderReloadPid(
387
+ req.headers.get(LOCAL_PROVIDER_RELOAD_EXPECTED_PID_HEADER),
388
+ );
389
+ if (expectedPid.kind !== "present" || expectedPid.pid !== local.pid) return false;
390
+ const expiresAtRaw = req.headers.get(LOCAL_PROVIDER_RELOAD_EXPIRES_AT_HEADER);
391
+ if (!expiresAtRaw || !/^[1-9]\d*$/.test(expiresAtRaw)) return false;
392
+ const expiresAt = Number(expiresAtRaw);
393
+ if (!Number.isSafeInteger(expiresAt)) return false;
394
+ const name = req.headers.get(LOCAL_PROVIDER_RELOAD_NAME_HEADER);
395
+ const capability = req.headers.get(LOCAL_PROVIDER_RELOAD_CAPABILITY_HEADER);
396
+ const now = Date.now();
397
+ if (!verifyLocalProviderReloadCapability(
398
+ local.attestationSecret,
399
+ req.headers.get(LOCAL_PROVIDER_RELOAD_NONCE_HEADER),
400
+ req.method,
401
+ url.pathname,
402
+ name,
403
+ local.pid,
404
+ local.port,
405
+ expiresAt,
406
+ capability,
407
+ now,
408
+ )) return false;
409
+ for (const [consumed, retainedUntil] of consumedLocalProviderReloadCapabilities) {
410
+ if (retainedUntil <= now) consumedLocalProviderReloadCapabilities.delete(consumed);
411
+ }
412
+ if (!capability || consumedLocalProviderReloadCapabilities.has(capability)) return false;
413
+ if (consumedLocalProviderReloadCapabilities.size >= LOCAL_PROVIDER_RELOAD_REPLAY_LIMIT) return false;
414
+ consumedLocalProviderReloadCapabilities.set(capability, expiresAt);
415
+ admittedLocalProviderReloadRequests.add(req);
416
+ return true;
417
+ }
418
+
419
+ /**
420
+ * The principal for a request that already passed `requireManagementAuth`. Kept as a
421
+ * separate resolution (rather than a changed return type) so every existing caller
422
+ * keeps its `Response | null` contract. Browser and admin principals are derived
423
+ * from the same session table and CSRF comparison the gate uses; the restart
424
+ * principal is derived from the same process-scoped capability check.
425
+ */
426
+ export function managementPrincipal(
427
+ req: Request,
428
+ state: ManagementAuthState,
429
+ config?: OcxConfig,
430
+ local?: LocalManagementAuthContext,
431
+ ): ManagementPrincipal | null {
432
+ if (hasSystemRestartCapability(req, local)) return "system-restart-capability";
433
+ if (hasLocalProviderReloadCapability(req, local)) return "local-provider-reload-capability";
434
+ if (hasLocalReadCapability(req, local)) return "local-read-capability";
435
+ if (!state.available) return null;
436
+ const actual = req.headers.get("x-opencodex-api-key")?.trim()
437
+ || req.headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
438
+ if (!actual) return null;
439
+ if (equalSecret(actual, state.token)) return "admin-token";
440
+ if (!config) return null;
441
+ removeExpiredSessions(state);
442
+ return state.sessions.has(actual) ? "gui-session" : null;
443
+ }
444
+
234
445
  export function requireManagementAuth(
235
446
  req: Request,
236
447
  state: ManagementAuthState,
237
448
  config?: OcxConfig,
449
+ local?: LocalManagementAuthContext,
238
450
  ): Response | null {
451
+ if (hasSystemRestartCapability(req, local)) return null;
452
+ if (hasLocalProviderReloadCapability(req, local)) return null;
453
+ if (hasLocalReadCapability(req, local)) return null;
239
454
  if (!state.available) {
240
455
  return Response.json({
241
456
  error: "management API unavailable",
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Optional per-attempt route-identity linker.
3
+ *
4
+ * Compatibility Lab attaches an opaque route-subject digest to request attempts so its
5
+ * passive-production surface (CL-09) can correlate them later. That is an opt-in
6
+ * subsystem, so the core request path holds only a slot: null on installs that never
7
+ * activate Lab, which is every install without a routing profile.
8
+ *
9
+ * Contract for any registered implementation: synchronous, free of side effects with
10
+ * respect to the request, and non-throwing. The upstream request must never be delayed,
11
+ * retried, or altered by identity linkage. The try/catch lives here rather than at the
12
+ * call site so the guarantee belongs to the mechanism instead of being restated by every
13
+ * caller.
14
+ *
15
+ * See devlog/_fin/260814_lab_core_decoupling/020_request_path_gate.md
16
+ */
17
+ import type { OcxConfig, OcxProviderConfig } from "../types";
18
+ import type { InboundWire } from "../providers/registry";
19
+
20
+ export type PassiveRouteLinker = (
21
+ config: OcxConfig,
22
+ providerName: string,
23
+ modelId: string,
24
+ routed: OcxProviderConfig,
25
+ inboundWire: InboundWire,
26
+ ) => string | null;
27
+
28
+ let linker: PassiveRouteLinker | null = null;
29
+
30
+ /** Install the linker. Returns a detach function. */
31
+ export function setPassiveRouteLinker(next: PassiveRouteLinker): () => void {
32
+ linker = next;
33
+ return () => {
34
+ // Only detach our own registration: a later activation may have replaced it.
35
+ if (linker === next) linker = null;
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Resolve the attempt identity, or null when no subsystem is active.
41
+ * Never throws: linkage is best-effort metadata and must not affect the request.
42
+ */
43
+ export function resolvePassiveRouteSubjectId(
44
+ config: OcxConfig,
45
+ providerName: string,
46
+ modelId: string,
47
+ routed: OcxProviderConfig,
48
+ inboundWire: InboundWire,
49
+ ): string | null {
50
+ if (!linker) return null;
51
+ try {
52
+ return linker(config, providerName, modelId, routed, inboundWire);
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ /** True when an optional subsystem has installed a linker. Test/diagnostic use. */
59
+ export function hasPassiveRouteLinker(): boolean {
60
+ return linker !== null;
61
+ }
62
+
63
+ /** Test-only reset. */
64
+ export function resetPassiveRouteLinkerForTests(): void {
65
+ linker = null;
66
+ }
@@ -9,7 +9,7 @@
9
9
  * (non-ocx, non-allowlisted) processes are never killed.
10
10
  */
11
11
  import { execFileSync } from "node:child_process";
12
- import { verifyPidIdentity } from "../config";
12
+ import { verifyPidIdentity } from "../config/process-state";
13
13
  import { isProcessAlive, killProxy } from "../lib/process-control";
14
14
  import { isPortAvailable, type WaitForPortOptions } from "./ports";
15
15
  import { dropWindowsTcpRowsForLocalPort } from "./windows-tcp-drop";
@@ -57,6 +57,16 @@ export type FindAvailablePortOptions = {
57
57
  * update restart cannot hop to a random ephemeral listener (PR #152 gap).
58
58
  */
59
59
  allowEphemeralFallback?: boolean;
60
+ /**
61
+ * A port this selection must never return, even when it is free (#1102).
62
+ *
63
+ * The unauthenticated loopback listener binds a fixed port from config. If the public
64
+ * listener took that port first — via an explicit `--port`, a `config.port` of 0, or the
65
+ * ephemeral fallback happening to land on it — the loopback bind would then fail with
66
+ * EADDRINUSE, and the startup transaction would roll back a public listener that had
67
+ * nothing wrong with it. Excluding the port here fails the right thing at the right time.
68
+ */
69
+ reservedPort?: number;
60
70
  };
61
71
 
62
72
  export class PortUnavailableError extends Error {
@@ -75,6 +85,12 @@ export async function findAvailablePort(
75
85
  ): Promise<number> {
76
86
  const preferRetryMs = opts.preferRetryMs ?? 0;
77
87
  const allowEphemeral = opts.allowEphemeralFallback !== false;
88
+ const reserved = opts.reservedPort;
89
+ // An explicit preference for the reserved port is a configuration mistake, not a busy
90
+ // socket: retrying or hopping would hide it. Refuse before probing anything.
91
+ if (reserved !== undefined && preferredPort === reserved) {
92
+ throw new PortUnavailableError(preferredPort, hostname);
93
+ }
78
94
  // Port 0 asks the OS to select an ephemeral port. Resolve it to that concrete
79
95
  // port here so callers never persist or advertise an unusable `:0` endpoint.
80
96
  if (preferredPort > 0 && preferRetryMs > 0) {
@@ -92,7 +108,31 @@ export async function findAvailablePort(
92
108
  throw new PortUnavailableError(preferredPort, hostname);
93
109
  }
94
110
 
95
- return await new Promise((resolve, reject) => {
111
+ // Bounded, not recursive. The OS can hand back the reserved port, and a redraw practically
112
+ // always differs — but "practically always" is not a termination argument, and an unbounded
113
+ // async recursion has no way to stop if the assumption is ever wrong.
114
+ for (let attempt = 0; attempt < EPHEMERAL_REDRAW_LIMIT; attempt += 1) {
115
+ const port = await allocateEphemeralPort(hostname);
116
+ if (port !== reserved) return port;
117
+ }
118
+ throw new Error("failed to allocate an available port");
119
+ }
120
+
121
+ /** How many times an ephemeral draw may come back reserved before we give up. */
122
+ const EPHEMERAL_REDRAW_LIMIT = 8;
123
+
124
+ /** Test seam: replace the OS ephemeral allocator so the redraw path is reachable. */
125
+ let ephemeralAllocator: ((hostname: string) => Promise<number>) | null = null;
126
+
127
+ export function setEphemeralPortAllocatorForTests(
128
+ allocator: ((hostname: string) => Promise<number>) | null,
129
+ ): void {
130
+ ephemeralAllocator = allocator;
131
+ }
132
+
133
+ async function allocateEphemeralPort(hostname: string): Promise<number> {
134
+ if (ephemeralAllocator) return ephemeralAllocator(hostname);
135
+ return await new Promise<number>((resolve, reject) => {
96
136
  const server = createServer();
97
137
  server.once("error", reject);
98
138
  server.once("listening", () => {
@@ -9,7 +9,9 @@
9
9
  *
10
10
  * Lives outside cli.ts (which dispatches argv at module top level) so tests can import it.
11
11
  */
12
- import { loadConfig, readAlivePid, readRuntimePort, verifyPidIdentity } from "../config";
12
+ import { loadConfig } from "../config";
13
+ import { readAlivePid, readRuntimePort, verifyPidIdentity } from "../config/process-state";
14
+ import { directLocalHttpFetch } from "./direct-local-http";
13
15
 
14
16
  export interface HealthzIdentity {
15
17
  service?: unknown;
@@ -17,6 +19,9 @@ export interface HealthzIdentity {
17
19
  version?: unknown;
18
20
  uptime?: unknown;
19
21
  pid?: unknown;
22
+ port?: unknown;
23
+ restartCapability?: unknown;
24
+ providerReloadCapability?: unknown;
20
25
  }
21
26
 
22
27
  export interface LivenessIo {
@@ -47,6 +52,9 @@ export interface LivenessIo {
47
52
  nowFn?: () => number;
48
53
  }
49
54
 
55
+ /** Default per-probe fetch ceiling shared by liveness and readiness probes. */
56
+ export const DEFAULT_PROBE_TIMEOUT_MS = 750;
57
+
50
58
  /** Default probe options for service stop / orphan cleanup — a just-bound proxy can miss a single 750ms probe. */
51
59
  export const SERVICE_STOP_LIVENESS: Pick<LivenessIo, "timeoutMs" | "attempts"> = {
52
60
  timeoutMs: 1500,
@@ -60,6 +68,14 @@ export interface LiveProxy {
60
68
  hostname?: string;
61
69
  /** Whether the successful probe used runtime-port metadata or the configured listen port. */
62
70
  source: "runtime" | "config";
71
+ /**
72
+ * Version the live proxy reported on `/healthz`, when it reported one.
73
+ *
74
+ * Carried so a stale `ocx` on PATH can be detected without a second request: the
75
+ * identity probe already parsed and validated this body. Absent for a legacy proxy whose
76
+ * healthz body predates the field.
77
+ */
78
+ version?: string;
63
79
  }
64
80
 
65
81
  /**
@@ -91,11 +107,11 @@ export async function proxyIdentityAt(
91
107
  port: number,
92
108
  opts: { hostname?: string; expectedPid?: number } = {},
93
109
  io: LivenessIo = {},
94
- ): Promise<{ pid: number | null } | null> {
95
- const fetchFn = io.fetchFn ?? fetch;
110
+ ): Promise<{ pid: number | null; version?: string } | null> {
111
+ const fetchFn = io.fetchFn ?? directLocalHttpFetch;
96
112
  const sleepFn = io.sleepFn ?? ((ms: number) => new Promise<void>(r => setTimeout(r, ms)));
97
113
  const nowFn = io.nowFn ?? Date.now;
98
- const baseTimeoutMs = io.timeoutMs ?? 750;
114
+ const baseTimeoutMs = io.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
99
115
  const requestedAttempts = Math.trunc(io.attempts ?? 1);
100
116
  const attempts = Number.isNaN(requestedAttempts)
101
117
  ? 1
@@ -114,7 +130,9 @@ export async function proxyIdentityAt(
114
130
  if (!isOpencodexHealthz(body)) return null;
115
131
  const pid = typeof body?.pid === "number" ? body.pid : null;
116
132
  if (opts.expectedPid !== undefined && pid !== null && pid !== opts.expectedPid) return null;
117
- return { pid };
133
+ // Guarded the same way `pid` is: a non-string version is absent, not coerced.
134
+ const version = typeof body?.version === "string" ? body.version : undefined;
135
+ return version === undefined ? { pid } : { pid, version };
118
136
  } catch {
119
137
  // Transport failure (timeout / refused) — retry while budget remains; a proxy that
120
138
  // has only just begun listening can miss a single short probe (#764).
@@ -138,6 +156,11 @@ export async function findLiveProxy(io: LivenessIo = {}): Promise<LiveProxy | nu
138
156
  const verifyPidFn = io.verifyPidFn ?? verifyPidIdentity;
139
157
  const readRuntimeFn = io.readRuntimeFn ?? readRuntimePort;
140
158
  const configFn = io.configFn ?? loadConfig;
159
+ const nowFn = io.nowFn ?? Date.now;
160
+ const deadlineAt = io.deadlineAt;
161
+ const probeIo: LivenessIo = io;
162
+ const budgetExhausted = (): boolean =>
163
+ deadlineAt !== undefined && nowFn() >= deadlineAt;
141
164
 
142
165
  // The cheap pid is discovery-only. Before it can appear in a returned (killable) result
143
166
  // it must pass the full identity check AND the verifier must echo the exact candidate —
@@ -160,13 +183,20 @@ export async function findLiveProxy(io: LivenessIo = {}): Promise<LiveProxy | nu
160
183
  if (pid) {
161
184
  const runtime = readRuntimeFn(pid);
162
185
  if (runtime?.port) {
186
+ if (budgetExhausted()) return null;
163
187
  probedPort = runtime.port;
164
- const identity = await proxyIdentityAt(runtime.port, { hostname: runtime.hostname, expectedPid: pid }, io);
188
+ const identity = await proxyIdentityAt(runtime.port, { hostname: runtime.hostname, expectedPid: pid }, probeIo);
165
189
  if (identity) {
166
190
  // healthz confirmed the pid itself → trusted; a pidless legacy body did not,
167
191
  // so the cheap pid must pass full identity verification before it is returned.
168
192
  const trusted = identity.pid === pid ? pid : killablePid(pid);
169
- return { pid: trusted, port: runtime.port, hostname: runtime.hostname, source: "runtime" };
193
+ return {
194
+ pid: trusted,
195
+ port: runtime.port,
196
+ hostname: runtime.hostname,
197
+ source: "runtime",
198
+ ...(identity.version === undefined ? {} : { version: identity.version }),
199
+ };
170
200
  }
171
201
  }
172
202
  }
@@ -176,26 +206,147 @@ export async function findLiveProxy(io: LivenessIo = {}): Promise<LiveProxy | nu
176
206
  // identity-probe it so ensure/update/stop see the live proxy instead of shadowing it.
177
207
  const record = readRuntimeFn();
178
208
  if (record?.port && record.port !== probedPort) {
209
+ if (budgetExhausted()) return null;
179
210
  const expectedPid = typeof record.pid === "number" ? record.pid : undefined;
180
- const identity = await proxyIdentityAt(record.port, { hostname: record.hostname, expectedPid }, io);
211
+ const identity = await proxyIdentityAt(record.port, { hostname: record.hostname, expectedPid }, probeIo);
181
212
  // Only the healthz-reported pid is authoritative here. The record's pid may be stale
182
213
  // (its process dead, the port reused by a pidless legacy proxy) — synthesizing it
183
214
  // would hand destructive callers (stopProxy → kill fallback) a reusable pid.
184
215
  if (identity) {
185
- return { pid: verifiedReportedPid(identity.pid), port: record.port, hostname: record.hostname, source: "runtime" };
216
+ return {
217
+ pid: verifiedReportedPid(identity.pid),
218
+ port: record.port,
219
+ hostname: record.hostname,
220
+ source: "runtime",
221
+ ...(identity.version === undefined ? {} : { version: identity.version }),
222
+ };
186
223
  }
187
224
  }
188
225
 
189
226
  const config = configFn();
190
227
  const port = config.port ?? 10100;
191
- const identity = await proxyIdentityAt(port, { hostname: config.hostname }, io);
228
+ if (budgetExhausted()) return null;
229
+ const identity = await proxyIdentityAt(port, { hostname: config.hostname }, probeIo);
192
230
  if (identity) {
193
231
  return {
194
232
  pid: verifiedReportedPid(identity.pid) ?? killablePid(pid),
195
233
  port,
196
234
  hostname: config.hostname,
197
235
  source: "config",
236
+ ...(identity.version === undefined ? {} : { version: identity.version }),
198
237
  };
199
238
  }
200
239
  return null;
201
240
  }
241
+
242
+ // ─────────────────────────────────────────────────────────────────────────────
243
+ // Readiness (/readyz) strict probe.
244
+ //
245
+ // Liveness (/healthz) confirms the process answers; readiness confirms the
246
+ // post-startup Codex sync has settled. A readiness probe is identity-checked the
247
+ // same way liveness is, AND additionally enforces the full /readyz contract so
248
+ // an adversarial or malformed body can never count as ready:
249
+ //
250
+ // - HTTP 200 is required for status="ready"; HTTP 503 is required for pending
251
+ // or failed. Any other HTTP/body-status pairing is an invalid contract.
252
+ // - body.service must be exactly "opencodex".
253
+ // - body.version must be a non-empty string.
254
+ // - body.uptime must be a finite nonnegative number.
255
+ // - body.pid must be a positive integer; when `expectedPid` is supplied it must
256
+ // match exactly.
257
+ // - body.port must be an integer in 1..65535 and equal the probed port.
258
+ // - body.status must be exactly one of pending|ready|failed.
259
+ //
260
+ // Any unreachable, foreign, legacy, malformed, mismatched, or self-inconsistent
261
+ // response returns `null` so callers can never treat an invalid identity/contract
262
+ // as ready.
263
+ // ─────────────────────────────────────────────────────────────────────────────
264
+
265
+ interface ReadyzBody {
266
+ service?: unknown;
267
+ version?: unknown;
268
+ uptime?: unknown;
269
+ pid?: unknown;
270
+ port?: unknown;
271
+ status?: unknown;
272
+ }
273
+
274
+ export interface ReadinessProbeResult {
275
+ /** True ONLY for a valid 200 + status="ready" body with a matching pid. */
276
+ ready: boolean;
277
+ /** Fixed sanitized status. A foreign/unreadable body yields a `null` RESULT, never a `null` status. */
278
+ status: "ready" | "pending" | "failed";
279
+ /** Positive integer pid from a valid body. */
280
+ pid: number;
281
+ /** Integer port from a valid body. */
282
+ port: number;
283
+ }
284
+
285
+ export interface ReadinessProbeIo {
286
+ fetchFn?: typeof fetch;
287
+ timeoutMs?: number;
288
+ }
289
+
290
+ const READYZ_STATUS_VALUES = new Set<"ready" | "pending" | "failed">(["ready", "pending", "failed"]);
291
+
292
+ /**
293
+ * Validate a parsed /readyz body against the strict contract. Returns the
294
+ * sanitized probe result, or `null` when the body is foreign, legacy,
295
+ * malformed, or fails the pid/port checks. Pure (no I/O) so it is fully
296
+ * deterministic and unit-testable.
297
+ */
298
+ export function validateReadyzBody(
299
+ body: unknown,
300
+ port: number,
301
+ opts: { expectedPid?: number } = {},
302
+ ): ReadinessProbeResult | null {
303
+ if (!body || typeof body !== "object") return null;
304
+ const b = body as ReadyzBody;
305
+ if (b.service !== "opencodex") return null;
306
+ if (typeof b.version !== "string" || b.version.length === 0) return null;
307
+ if (typeof b.uptime !== "number" || !Number.isFinite(b.uptime) || b.uptime < 0) return null;
308
+ if (typeof b.pid !== "number" || !Number.isInteger(b.pid) || b.pid <= 0) return null;
309
+ if (
310
+ typeof b.port !== "number"
311
+ || !Number.isInteger(b.port)
312
+ || b.port < 1
313
+ || b.port > 65535
314
+ || b.port !== port
315
+ ) return null;
316
+ if (typeof b.status !== "string" || !READYZ_STATUS_VALUES.has(b.status as "ready" | "pending" | "failed")) return null;
317
+ const status = b.status as "ready" | "pending" | "failed";
318
+ if (opts.expectedPid !== undefined && b.pid !== opts.expectedPid) return null;
319
+ return { ready: status === "ready", status, pid: b.pid, port: b.port };
320
+ }
321
+
322
+ /**
323
+ * Identity- and contract-checked /readyz probe. Returns `null` when the
324
+ * endpoint is unreachable or the body fails the strict contract (foreign 200,
325
+ * legacy health-only body, non-JSON, missing/malformed/mismatched fields,
326
+ * wrong port/pid, or an HTTP/body-status inconsistency). Returns
327
+ * `{ready:false, ...}` when the body is ours but pending or failed. Returns
328
+ * `{ready:true, ...}` ONLY for a valid 200 body with `status:"ready"` and (when
329
+ * requested) a matching pid.
330
+ */
331
+ export async function probeReadiness(
332
+ port: number,
333
+ opts: { hostname?: string; expectedPid?: number } = {},
334
+ io: ReadinessProbeIo = {},
335
+ ): Promise<ReadinessProbeResult | null> {
336
+ const fetchFn = io.fetchFn ?? directLocalHttpFetch;
337
+ try {
338
+ const res = await fetchFn(`http://${probeHostname(opts.hostname)}:${port}/readyz`, {
339
+ signal: AbortSignal.timeout(io.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS),
340
+ });
341
+ // Parse even on 503: /readyz returns JSON with a sanitized status while pending.
342
+ const body = (await res.json().catch(() => null)) as unknown;
343
+ const parsed = validateReadyzBody(body, port, opts);
344
+ if (!parsed) return null;
345
+ // HTTP/body-status consistency: ready requires 200; pending/failed require 503.
346
+ if (parsed.status === "ready" && res.status !== 200) return null;
347
+ if (parsed.status !== "ready" && res.status !== 503) return null;
348
+ return parsed;
349
+ } catch {
350
+ return null;
351
+ }
352
+ }