@bitkyc08/opencodex 2.80.0 → 2.82.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 (376) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +166 -73
  3. package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
  4. package/gui/dist/assets/Tray-BqHF5Trd.js +1 -0
  5. package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
  6. package/gui/dist/assets/index-D3FBCNiP.js +94 -0
  7. package/gui/dist/assets/quota-summary-DTBC8n7-.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/gui/dist/provider-icons/claude-green.svg +1 -0
  10. package/package.json +3 -2
  11. package/src/adapters/anthropic-model-contract.ts +8 -2
  12. package/src/adapters/anthropic.ts +8 -3
  13. package/src/adapters/codebuddy/mcp-server.ts +2 -25
  14. package/src/adapters/codebuddy/tool-bridge.ts +1 -7
  15. package/src/adapters/coding-agent/mcp-server.ts +33 -0
  16. package/src/adapters/coding-agent/protocol.ts +122 -17
  17. package/src/adapters/coding-agent/tool-catalog.ts +75 -0
  18. package/src/adapters/coding-agent/turn.ts +101 -44
  19. package/src/adapters/cursor/catalog.ts +8 -0
  20. package/src/adapters/cursor/effort-map.ts +2 -0
  21. package/src/adapters/devin/live-models.ts +2 -0
  22. package/src/adapters/kiro-retry.ts +9 -4
  23. package/src/adapters/openai-chat/messages.ts +7 -6
  24. package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
  25. package/src/adapters/openai-responses/passthrough.ts +9 -4
  26. package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
  27. package/src/adapters/physical-send.ts +8 -5
  28. package/src/adapters/qoder/adapter.ts +41 -6
  29. package/src/adapters/qoder/scaffold-guard.ts +2 -3
  30. package/src/adapters/zed.ts +16 -1
  31. package/src/claude/agents-inject.ts +14 -3
  32. package/src/claude/context-windows.ts +19 -17
  33. package/src/claude/desktop-3p.ts +88 -12
  34. package/src/claude/desktop-profile.ts +2 -1
  35. package/src/claude/gateway-cache.ts +18 -1
  36. package/src/claude/inbound-cache-stabilize.ts +4 -9
  37. package/src/claude/inbound.ts +78 -3
  38. package/src/claude/intercept/cli-catalog.ts +3 -0
  39. package/src/claude/intercept/model-bindings.ts +1 -0
  40. package/src/claude/intercept/picker-ca-persistence.ts +7 -6
  41. package/src/claude/intercept/picker-listener.ts +71 -4
  42. package/src/claude/intercept/picker-models.ts +14 -6
  43. package/src/claude/intercept/settings.ts +3 -0
  44. package/src/claude/long-context.ts +66 -0
  45. package/src/claude/model-info.ts +19 -16
  46. package/src/claude/outbound.ts +94 -9
  47. package/src/claude/subagent-model.ts +40 -11
  48. package/src/cli/account-anthropic-threshold.ts +14 -5
  49. package/src/cli/account-api.ts +3 -1
  50. package/src/cli/account-extended.ts +13 -9
  51. package/src/cli/account-policy-dto.ts +1 -1
  52. package/src/cli/account-policy.ts +13 -4
  53. package/src/cli/account.ts +3 -3
  54. package/src/cli/agent.ts +9 -5
  55. package/src/cli/aside-profile-recovery.ts +33 -0
  56. package/src/cli/capabilities-accounts.ts +2 -2
  57. package/src/cli/capabilities-agents-routing.ts +24 -0
  58. package/src/cli/capabilities-base.ts +5 -5
  59. package/src/cli/capabilities-integrations.ts +18 -2
  60. package/src/cli/cli-path-diagnostics.ts +132 -0
  61. package/src/cli/codex-shim-autorestore.ts +3 -0
  62. package/src/cli/desktop-runtime-guidance.ts +65 -0
  63. package/src/cli/dispatch.ts +15 -1
  64. package/src/cli/doctor.ts +16 -3
  65. package/src/cli/help.ts +4 -1
  66. package/src/cli/index.ts +25 -14
  67. package/src/cli/integration-aside-sync.ts +2 -1
  68. package/src/cli/integration-plan-dto.ts +1 -0
  69. package/src/cli/integration-preview.ts +8 -1
  70. package/src/cli/integrations.ts +48 -6
  71. package/src/cli/message-args.ts +34 -0
  72. package/src/cli/message-command.ts +43 -0
  73. package/src/cli/message-runtime.ts +6 -0
  74. package/src/cli/observe.ts +3 -1
  75. package/src/cli/provider.ts +29 -6
  76. package/src/cli/registry.ts +24 -2
  77. package/src/cli/resolve.ts +25 -2
  78. package/src/cli/root.ts +4 -2
  79. package/src/cli/status.ts +64 -12
  80. package/src/cli/update-restart-child.ts +4 -2
  81. package/src/cli/update-restart-home.ts +28 -10
  82. package/src/cli/update-restart-service-record.ts +116 -0
  83. package/src/cli/update-restart-supervision.ts +96 -0
  84. package/src/cli/update-restart.ts +59 -13
  85. package/src/cli/v2.ts +27 -20
  86. package/src/cli/version-skew-notice.ts +77 -0
  87. package/src/clients/config-export/commandcode.ts +131 -0
  88. package/src/clients/config-export/contracts.ts +12 -0
  89. package/src/clients/config-export.ts +64 -1
  90. package/src/clients/lazycodex.ts +1 -1
  91. package/src/clients/omo-role-models.ts +32 -7
  92. package/src/codex/auth-api/account-list.ts +11 -3
  93. package/src/codex/auth-api/account-selection.ts +107 -0
  94. package/src/codex/auth-api/main-account-probe.ts +25 -8
  95. package/src/codex/auth-api/pool-mode-gate.ts +33 -9
  96. package/src/codex/auth-api/pool-quota-probe.ts +15 -1
  97. package/src/codex/auth-api/routes.ts +11 -49
  98. package/src/codex/auth-context.ts +20 -1
  99. package/src/codex/autostart-health.ts +12 -3
  100. package/src/codex/catalog/auto-review.ts +13 -6
  101. package/src/codex/catalog/build-entries.ts +33 -7
  102. package/src/codex/catalog/bundled.ts +155 -26
  103. package/src/codex/catalog/combo-member.ts +3 -2
  104. package/src/codex/catalog/control-plane.ts +45 -0
  105. package/src/codex/catalog/effort.ts +37 -6
  106. package/src/codex/catalog/gather-capture.ts +10 -3
  107. package/src/codex/catalog/model-hints.ts +15 -8
  108. package/src/codex/catalog/model-visibility.ts +3 -1
  109. package/src/codex/catalog/operator-tiers.ts +38 -0
  110. package/src/codex/catalog/parsing.ts +2 -1
  111. package/src/codex/catalog/provider-models.ts +58 -44
  112. package/src/codex/catalog/retained-sync.ts +7 -5
  113. package/src/codex/catalog/routed-gather.ts +15 -1
  114. package/src/codex/catalog-auto-refresh-sources.ts +3 -4
  115. package/src/codex/catalog-auto-refresh.ts +19 -0
  116. package/src/codex/catalog.ts +1 -1
  117. package/src/codex/codex-home-owner.ts +3 -0
  118. package/src/codex/config-write-lock.ts +333 -0
  119. package/src/codex/control-plane-models.ts +6 -0
  120. package/src/codex/convergence.ts +2 -1
  121. package/src/codex/features.ts +257 -200
  122. package/src/codex/inject/config-write-section.ts +34 -0
  123. package/src/codex/inject/multi-agent-v2.ts +14 -6
  124. package/src/codex/inject/remove.ts +36 -11
  125. package/src/codex/inject/restore.ts +85 -20
  126. package/src/codex/inject-coordination.ts +15 -6
  127. package/src/codex/inject.ts +62 -20
  128. package/src/codex/journal.ts +62 -17
  129. package/src/codex/main-account-cache.ts +43 -0
  130. package/src/codex/paths.ts +2 -0
  131. package/src/codex/prepared-home.ts +18 -0
  132. package/src/codex/prompt-journal.ts +25 -12
  133. package/src/codex/prompt-layers.ts +49 -26
  134. package/src/codex/prompt-lock-claim.ts +134 -0
  135. package/src/codex/prompt-lock-evidence.ts +26 -0
  136. package/src/codex/prompt-lock-io.ts +16 -0
  137. package/src/codex/prompt-lock-owner.ts +130 -0
  138. package/src/codex/prompt-lock.ts +84 -53
  139. package/src/codex/routing/failure-window.ts +146 -0
  140. package/src/codex/routing/selection.ts +4 -1
  141. package/src/codex/routing.ts +13 -17
  142. package/src/codex/runtime.ts +106 -18
  143. package/src/codex/shim.ts +45 -19
  144. package/src/combos/jev-decision-contract.ts +25 -2
  145. package/src/combos/jev-service-exchange.ts +281 -0
  146. package/src/combos/jev.ts +34 -219
  147. package/src/combos/types.ts +1 -1
  148. package/src/config/atomic-write.ts +1 -1
  149. package/src/config/codex-ws-reuse-setting.ts +10 -0
  150. package/src/config/diagnostics.ts +27 -16
  151. package/src/config/live-reconcile.ts +17 -0
  152. package/src/config/load-degrade.ts +42 -0
  153. package/src/config/rebase-provenance.ts +11 -0
  154. package/src/config/schema/anthropic-account-pool.ts +143 -0
  155. package/src/config/schema/config-schema.ts +11 -1
  156. package/src/config/schema/leaf-validators.ts +4 -0
  157. package/src/config.ts +2 -0
  158. package/src/generated/compatibility-version.json +566 -298
  159. package/src/generated/model-metadata.ts +7 -6
  160. package/src/grok/inject.ts +50 -4
  161. package/src/grok/sync.ts +2 -1
  162. package/src/images/loop.ts +30 -11
  163. package/src/integrations/aside-profiles.ts +10 -3
  164. package/src/integrations/catalog-refresh.ts +19 -3
  165. package/src/integrations/mutation-plan.ts +7 -0
  166. package/src/integrations/registry.ts +8 -0
  167. package/src/integrations/state.ts +9 -2
  168. package/src/integrations/writer.ts +17 -5
  169. package/src/lib/bun-path-runtime.d.mts +32 -0
  170. package/src/lib/bun-path-runtime.mjs +95 -0
  171. package/src/lib/bun-runtime-preflight.ts +140 -0
  172. package/src/lib/desktop-cli-handoff.d.mts +20 -0
  173. package/src/lib/desktop-cli-handoff.mjs +92 -0
  174. package/src/lib/desktop-cli-record.d.mts +29 -0
  175. package/src/lib/desktop-cli-record.mjs +102 -0
  176. package/src/lib/lab-live-route-production.ts +9 -1
  177. package/src/lib/request-execution-budget.ts +244 -58
  178. package/src/lib/spend-pool-alias-validation.ts +79 -0
  179. package/src/lib/spend-pool-continuity.ts +34 -0
  180. package/src/lib/spend-reservation-ledger.ts +412 -50
  181. package/src/lib/state-store-registrations.ts +20 -3
  182. package/src/lib/synced-state-location.ts +46 -6
  183. package/src/lib/test-home-guard.ts +87 -1
  184. package/src/lib/translator-budget.ts +17 -4
  185. package/src/lib/upstream-retry.ts +17 -5
  186. package/src/lib/windows-atomic-replace.ts +38 -16
  187. package/src/lib/windows-elevation.ts +6 -0
  188. package/src/lib/windows-secret-acl.ts +92 -25
  189. package/src/lib/winsw.ts +2 -3
  190. package/src/lib/workflow-budget.ts +53 -11
  191. package/src/messaging/budget.ts +45 -0
  192. package/src/messaging/discovery.ts +57 -0
  193. package/src/messaging/envelope.ts +49 -0
  194. package/src/messaging/input.ts +37 -0
  195. package/src/messaging/rpc.ts +196 -0
  196. package/src/messaging/send.ts +58 -0
  197. package/src/messaging/socket.ts +72 -0
  198. package/src/messaging/types.ts +29 -0
  199. package/src/oauth/anthropic-account-refusal.ts +90 -18
  200. package/src/oauth/anthropic-account-threshold.ts +8 -2
  201. package/src/oauth/anthropic-continuity.ts +15 -1
  202. package/src/oauth/anthropic-model-quota.ts +120 -48
  203. package/src/oauth/anthropic-model-routes.ts +11 -3
  204. package/src/oauth/anthropic-oauth-definitions.ts +98 -0
  205. package/src/oauth/anthropic-pool-config.ts +30 -0
  206. package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
  207. package/src/oauth/anthropic-routing.ts +1202 -866
  208. package/src/oauth/anthropic-send-ownership.ts +41 -0
  209. package/src/oauth/anthropic.ts +4 -1
  210. package/src/oauth/chatgpt.ts +113 -41
  211. package/src/oauth/generic-account-failover.ts +3 -1
  212. package/src/oauth/health.ts +8 -6
  213. package/src/oauth/index.ts +96 -175
  214. package/src/oauth/kiro.ts +22 -9
  215. package/src/oauth/login-cli.ts +2 -0
  216. package/src/oauth/model-discovery-auth.ts +43 -0
  217. package/src/oauth/pool-kernel.ts +11 -5
  218. package/src/oauth/pool-settings-capability.ts +5 -3
  219. package/src/oauth/store-anthropic-instance.ts +87 -0
  220. package/src/oauth/store.ts +68 -10
  221. package/src/oauth/token-guardian.ts +2 -0
  222. package/src/oauth/types.ts +2 -0
  223. package/src/protocols/plan-snapshot.ts +5 -0
  224. package/src/protocols/settings.ts +20 -5
  225. package/src/providers/anthropic-instance-id.ts +47 -0
  226. package/src/providers/anthropic-instance.ts +48 -0
  227. package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
  228. package/src/providers/azure-model-metadata.ts +115 -0
  229. package/src/providers/derive.ts +23 -0
  230. package/src/providers/kiro-models.ts +3 -0
  231. package/src/providers/label.ts +1 -1
  232. package/src/providers/model-rename-fields.ts +2 -0
  233. package/src/providers/quota/account-cache.ts +37 -11
  234. package/src/providers/quota/anthropic-account-quota.ts +93 -0
  235. package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
  236. package/src/providers/quota/report-cache.ts +24 -1
  237. package/src/providers/quota/vendor-probes-key.ts +127 -17
  238. package/src/providers/quota/vendor-probes-oauth.ts +46 -14
  239. package/src/providers/quota.ts +23 -33
  240. package/src/providers/registry/entries-core.ts +35 -30
  241. package/src/providers/registry/model-ids.ts +1 -0
  242. package/src/providers/registry/model-seeds.ts +7 -4
  243. package/src/providers/registry/types.ts +3 -0
  244. package/src/providers/registry.ts +3 -3
  245. package/src/responses/reasoning-envelope.ts +29 -1
  246. package/src/responses/reasoning-replay-cache.ts +17 -1
  247. package/src/responses/spill-store.ts +20 -3
  248. package/src/responses/state/metrics.ts +95 -0
  249. package/src/responses/state/snapshot-codec.ts +6 -0
  250. package/src/responses/state/spill-failure.ts +12 -0
  251. package/src/responses/state/spill-queue.ts +13 -5
  252. package/src/responses/state/unforced-store-false.ts +55 -0
  253. package/src/responses/state.ts +68 -76
  254. package/src/responses/task-input.ts +17 -0
  255. package/src/router.ts +28 -4
  256. package/src/routing/compatibility/assemble.ts +1 -1
  257. package/src/routing/quota.ts +7 -3
  258. package/src/server/auth-cors.ts +5 -0
  259. package/src/server/background-lifecycle.ts +27 -0
  260. package/src/server/chat-completions.ts +7 -1
  261. package/src/server/chat-native.ts +64 -28
  262. package/src/server/claude-messages.ts +59 -8
  263. package/src/server/gui-static.ts +38 -4
  264. package/src/server/inbound-body-admission.ts +1 -0
  265. package/src/server/index/claude-intercept-lifecycle.ts +3 -1
  266. package/src/server/index/serve-options.ts +10 -2
  267. package/src/server/index/spend-ledger-lifecycle.ts +12 -5
  268. package/src/server/index/startup-warnings.ts +19 -0
  269. package/src/server/index.ts +5 -2
  270. package/src/server/inference/context.ts +1 -1
  271. package/src/server/lifecycle.ts +1 -1
  272. package/src/server/management/agent-settings-routes.ts +123 -38
  273. package/src/server/management/anthropic-account-threshold.ts +12 -9
  274. package/src/server/management/anthropic-pool-settings.ts +72 -0
  275. package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
  276. package/src/server/management/codex-agent-role-routes.ts +4 -2
  277. package/src/server/management/codex-prompt-routes.ts +1 -0
  278. package/src/server/management/codex-role-auto-assign.ts +13 -2
  279. package/src/server/management/config-routes.ts +85 -14
  280. package/src/server/management/context.ts +2 -2
  281. package/src/server/management/decision-routes.ts +2 -1
  282. package/src/server/management/integration-routes.ts +25 -2
  283. package/src/server/management/oauth-account-routes.ts +79 -98
  284. package/src/server/management/provider-instance-ownership.ts +44 -0
  285. package/src/server/management/provider-routes.ts +21 -28
  286. package/src/server/management/route-registry.ts +3 -1
  287. package/src/server/management/routing-profile-routes.ts +4 -4
  288. package/src/server/management/shared.ts +3 -3
  289. package/src/server/management/system-restart.ts +44 -14
  290. package/src/server/management/system-routes.ts +24 -2
  291. package/src/server/management/vision-sidecar-options.ts +9 -6
  292. package/src/server/management/web-search-sidecar-options.ts +51 -2
  293. package/src/server/management-auth.ts +6 -0
  294. package/src/server/messages-native-eligibility.ts +3 -2
  295. package/src/server/messages-native-oauth.ts +91 -45
  296. package/src/server/messages-native-selector.ts +39 -0
  297. package/src/server/messages-native.ts +133 -38
  298. package/src/server/messages-response-headers.ts +14 -0
  299. package/src/server/request-log-conversation.ts +48 -5
  300. package/src/server/request-log.ts +36 -2
  301. package/src/server/responses/adapter-continuation.ts +31 -19
  302. package/src/server/responses/adapter-dispatch.ts +75 -24
  303. package/src/server/responses/antigravity-validation-refusal.ts +20 -6
  304. package/src/server/responses/codex-ws-exchange.ts +17 -2
  305. package/src/server/responses/codex-ws-pool.ts +100 -13
  306. package/src/server/responses/codex-ws-wire.ts +9 -0
  307. package/src/server/responses/combo-requested-effort.ts +3 -1
  308. package/src/server/responses/compact.ts +25 -8
  309. package/src/server/responses/compaction-recovery.ts +1 -1
  310. package/src/server/responses/core-codex-account.ts +22 -1
  311. package/src/server/responses/core-combo-native.ts +24 -4
  312. package/src/server/responses/core-combo.ts +38 -25
  313. package/src/server/responses/core-lifetime.ts +3 -36
  314. package/src/server/responses/core-normalize.ts +1 -0
  315. package/src/server/responses/core-options.ts +8 -1
  316. package/src/server/responses/core-replay.ts +78 -0
  317. package/src/server/responses/core.ts +3 -3
  318. package/src/server/responses/fetch-helpers.ts +26 -3
  319. package/src/server/responses/passthrough-delivery.ts +30 -4
  320. package/src/server/responses/passthrough-dispatch.ts +39 -9
  321. package/src/server/responses/request-prepare.ts +41 -5
  322. package/src/server/responses/request-send-budget.ts +150 -50
  323. package/src/server/responses/request-spend.ts +325 -33
  324. package/src/server/responses/request-transport.ts +132 -52
  325. package/src/server/responses/response-effects.ts +1 -0
  326. package/src/server/responses/run-turn-execution.ts +13 -3
  327. package/src/server/responses/sidecar-execution.ts +113 -22
  328. package/src/server/responses/sidecar-send-budget.ts +58 -0
  329. package/src/server/responses/ws-upstream.ts +19 -6
  330. package/src/server/responses-undeclared-tool-guard.ts +5 -1
  331. package/src/server/startup-health-cache.ts +2 -1
  332. package/src/server/workflow-refusal.ts +33 -0
  333. package/src/service/cli.ts +20 -4
  334. package/src/service/desktop-command-guard.ts +32 -0
  335. package/src/service/desktop-startup.ts +66 -40
  336. package/src/service/desktop-supervision.d.mts +26 -0
  337. package/src/service/desktop-supervision.mjs +140 -0
  338. package/src/service/managing-cli.ts +1 -0
  339. package/src/service/orchestration.ts +46 -20
  340. package/src/service/ownership-mutation-lease.d.mts +1 -1
  341. package/src/service/ownership-mutation-lease.mjs +5 -7
  342. package/src/service/repair.ts +25 -9
  343. package/src/service/state.ts +4 -3
  344. package/src/service/systemd.ts +17 -8
  345. package/src/service/windows-ops.ts +61 -22
  346. package/src/sidecar/anthropic-binding.ts +79 -0
  347. package/src/sidecar/auth.ts +76 -2
  348. package/src/storage/policy-job.ts +5 -1
  349. package/src/storage/policy-worker.ts +3 -1
  350. package/src/storage/policy.ts +9 -0
  351. package/src/types/anthropic-account-pool.ts +33 -0
  352. package/src/types/config.ts +33 -28
  353. package/src/types/provider.ts +5 -0
  354. package/src/types/request.ts +5 -0
  355. package/src/types/tools.ts +7 -0
  356. package/src/types/wire.ts +1 -0
  357. package/src/types.ts +4 -0
  358. package/src/update/index.ts +57 -9
  359. package/src/update/restart-ownership.ts +10 -3
  360. package/src/update/runtime-ownership.d.mts +6 -0
  361. package/src/update/runtime-ownership.mjs +23 -7
  362. package/src/usage/cost.ts +3 -2
  363. package/src/usage/expected-prices.ts +66 -14
  364. package/src/vision/anthropic-describe.ts +16 -3
  365. package/src/vision/index.ts +3 -0
  366. package/src/vision/plan.ts +19 -6
  367. package/src/web-search/alpha-search.ts +7 -1
  368. package/src/web-search/anthropic-executor.ts +16 -4
  369. package/src/web-search/index.ts +7 -3
  370. package/src/web-search/loop.ts +32 -7
  371. package/src/web-search/passthrough-bridge.ts +4 -1
  372. package/src/web-search/sidecar-providers.ts +4 -3
  373. package/gui/dist/assets/App-MbDVfUeC.js +0 -54
  374. package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
  375. package/gui/dist/assets/index-UJt6npXk.js +0 -94
  376. package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
@@ -1,3 +1,7 @@
1
+ import { existsSync } from "node:fs";
2
+ import { getConfigDir } from "../config";
3
+ import { durableBunRuntime, type DurableBunRuntime } from "../lib/bun-runtime";
4
+ import { assertSelectedRuntimeWritable, type RuntimePreflightDeps } from "../lib/bun-runtime-preflight";
1
5
  import { randomUUID } from "node:crypto";
2
6
  import { defaultWinswEntry, installWinswService } from "../lib/winsw";
3
7
  import { resolveWindowsTaskDiagnosticUserId, diagnoseService } from "./diagnostics";
@@ -14,6 +18,7 @@ import type { WindowsSchedulerTaskProbe } from "./windows-scheduler";
14
18
  import { taskXmlSection, taskXmlWithoutCommentsAndCdata, taskXmlElementCount, taskXmlOptionalValueEquals, windowsTaskRegistrationOwnedByAttempt, windowsTaskHasSessionRecoveryTriggers, windowsTaskRegistrationHealthy, windowsTaskRegistrationRefreshableLegacy } from "./windows-taskxml";
15
19
  import type { ExpectedWindowsTaskUserId } from "./windows-taskxml";
16
20
  import { win32 } from "node:path";
21
+ import { assertNoDesktopSupervision, createSupervisionLatch, type SupervisionInspector, type SupervisionLatch } from "./desktop-command-guard";
17
22
 
18
23
  /**
19
24
  * The two CLI verbs `repairService` serves. They differ on ONE platform and ONE case: a
@@ -22,16 +27,18 @@ import { win32 } from "node:path";
22
27
  */
23
28
  export type ServiceRepairVerb = "repair" | "restart";
24
29
 
25
- export interface RepairServiceDeps {
30
+ export interface RepairServiceDeps extends RuntimePreflightDeps {
31
+ inspectSupervision?: SupervisionInspector;
32
+ supervisionLatch?: SupervisionLatch;
26
33
  diagnose?: () => ServiceDiagnostic;
27
34
  assertEnv?: () => void;
28
35
  assertAuth?: () => void;
29
- writeSchedulerAssets?: () => void;
36
+ writeSchedulerAssets?: (runtime: DurableBunRuntime) => void;
30
37
  stopScheduler?: () => void;
31
38
  startScheduler?: () => void;
32
- writeSchedulerState?: () => void;
33
- writeNativeState?: () => void;
34
- repairNative?: () => void | Promise<void>;
39
+ writeSchedulerState?: (runtime: DurableBunRuntime) => void;
40
+ writeNativeState?: (runtime: DurableBunRuntime) => void;
41
+ repairNative?: (runtime: DurableBunRuntime) => void | Promise<void>;
35
42
  repairLaunchd?: () => LaunchdInstallOutcome | void;
36
43
  repairSystemd?: () => void;
37
44
  /** Restarts a launchd job the install path deliberately left alone. `restart` only. */
@@ -148,6 +155,8 @@ export async function assertSchedulerRegistrationBeforeStart(
148
155
  * macOS/Linux: re-run the user-level install/reload path.
149
156
  */
150
157
  export async function repairService(deps: RepairServiceDeps = {}): Promise<void> {
158
+ const latch = deps.supervisionLatch ?? createSupervisionLatch();
159
+ assertNoDesktopSupervision(deps.inspectSupervision, latch);
151
160
  const diagnose = deps.diagnose ?? diagnoseService;
152
161
  const platform = deps.platform ?? process.platform;
153
162
  const diag = diagnose();
@@ -176,14 +185,18 @@ export async function repairService(deps: RepairServiceDeps = {}): Promise<void>
176
185
 
177
186
  (deps.assertEnv ?? assertServiceEnvironmentMatchesInstall)();
178
187
  (deps.assertAuth ?? assertServiceAuthEnvironment)();
188
+ const runtime = Object.freeze({ ...(deps.selectRuntime ?? durableBunRuntime)() });
189
+ const configDir = (deps.configDir ?? getConfigDir)();
190
+ (deps.assertRuntimeWritable ?? assertSelectedRuntimeWritable)(runtime, configDir, { platform, rootWasAbsent: !existsSync(configDir) });
179
191
 
180
192
  const temporaryLauncher = (deps.launcherPathDiagnostic ?? (() => serviceLauncherPathDiagnostic(undefined, platform, true)))();
181
193
  if (temporaryLauncher) console.warn(`⚠️ ${temporaryLauncher}`);
182
194
 
183
195
  if (platform === "win32") {
184
196
  if (diag.backend === "native") {
185
- await (deps.repairNative ?? (() => installWinswService(defaultWinswEntry(serviceSourceDir))))();
186
- (deps.writeNativeState ?? (() => writeServiceInstallState("native")))();
197
+ await (deps.repairNative ?? ((selected: DurableBunRuntime) => installWinswService(defaultWinswEntry(serviceSourceDir, selected))))(runtime);
198
+ assertNoDesktopSupervision(deps.inspectSupervision, latch);
199
+ (deps.writeNativeState ?? ((selected: DurableBunRuntime) => writeServiceInstallState("native", undefined, {}, selected)))(runtime);
187
200
  return;
188
201
  }
189
202
  const readSchedulerXml = deps.readSchedulerXml ?? statusWindowsXml;
@@ -243,7 +256,7 @@ export async function repairService(deps: RepairServiceDeps = {}): Promise<void>
243
256
  );
244
257
  }
245
258
  try { (deps.stopScheduler ?? stopWindows)(); } catch { /* not running */ }
246
- (deps.writeSchedulerAssets ?? writeWindowsSchedulerAssets)();
259
+ (deps.writeSchedulerAssets ?? writeWindowsSchedulerAssets)(runtime);
247
260
  // Rewriting the on-disk assets does not touch the definition Task Scheduler holds, so a
248
261
  // task registered by an older version keeps its old triggers forever: status reports it
249
262
  // stale, tells the user to run repair, and repair changes nothing it complains about.
@@ -311,6 +324,7 @@ export async function repairService(deps: RepairServiceDeps = {}): Promise<void>
311
324
  }
312
325
  if (probe.status === "absent") {
313
326
  try {
327
+ assertNoDesktopSupervision(deps.inspectSupervision, latch);
314
328
  await (deps.restoreSchedulerIfAbsent ?? restoreWindowsSchedulerTaskIfAbsent)(registeredXml);
315
329
  restartExpectedXml = registeredXml;
316
330
  } catch (error) {
@@ -333,6 +347,7 @@ export async function repairService(deps: RepairServiceDeps = {}): Promise<void>
333
347
  "The Task Scheduler registration changed again before restart; the newer definition was preserved and not started.",
334
348
  "Task Scheduler state remained unreadable before restart; the registration was preserved and not started.",
335
349
  );
350
+ assertNoDesktopSupervision(deps.inspectSupervision, latch);
336
351
  (deps.startScheduler ?? startWindows)();
337
352
  } catch (error) {
338
353
  recoveryErrors.push(error);
@@ -358,8 +373,9 @@ export async function repairService(deps: RepairServiceDeps = {}): Promise<void>
358
373
  "Task Scheduler registration changed before restart; the current definition was preserved and not started.",
359
374
  "Task Scheduler registration became unreadable before restart; it was preserved and not started.",
360
375
  );
376
+ assertNoDesktopSupervision(deps.inspectSupervision, latch);
361
377
  (deps.startScheduler ?? startWindows)();
362
- (deps.writeSchedulerState ?? (() => writeServiceInstallState("scheduler")))();
378
+ (deps.writeSchedulerState ?? ((selected: DurableBunRuntime) => writeServiceInstallState("scheduler", undefined, {}, selected)))(runtime);
363
379
  return;
364
380
  }
365
381
  if (platform === "darwin") {
@@ -360,8 +360,8 @@ export function parseServiceInstallState(value: unknown): ServiceInstallState |
360
360
  * it describes the install that just ran. {@link ServiceInstallState.ownership} deliberately
361
361
  * is not part of it.
362
362
  */
363
- function installProvenanceRecord(backend: ServiceBackend, launcherPath?: string | null): ServiceInstallState {
364
- const { bun, cli } = cliEntry();
363
+ function installProvenanceRecord(backend: ServiceBackend, launcherPath: string | null | undefined, runtime: DurableBunRuntime): ServiceInstallState {
364
+ const { bun, cli } = cliEntry(runtime);
365
365
  const codexHome = currentCodexHome();
366
366
  return {
367
367
  version: 2,
@@ -391,9 +391,10 @@ export function writeServiceInstallState(
391
391
  backend: ServiceBackend = "scheduler",
392
392
  launcherPath?: string | null,
393
393
  deps: ServiceStateSwapDeps = {},
394
+ runtime: DurableBunRuntime = durableBunRuntime(),
394
395
  ): void {
395
396
  swapServiceInstallState(current => ({
396
- ...installProvenanceRecord(backend, launcherPath),
397
+ ...installProvenanceRecord(backend, launcherPath, runtime),
397
398
  ...preservedConsent(current),
398
399
  }), deps);
399
400
  }
@@ -102,13 +102,22 @@ WantedBy=default.target
102
102
 
103
103
  /** The per-user runtime dir systemd creates (holds the user-bus socket), or null. */
104
104
  function userRuntimeDir(): string | null {
105
- const fromEnv = process.env.XDG_RUNTIME_DIR;
106
- if (fromEnv && existsSync(fromEnv)) return fromEnv;
107
- if (typeof process.getuid === "function") {
108
- const candidate = `/run/user/${process.getuid()}`;
109
- if (existsSync(candidate)) return candidate;
105
+ const env = { ...process.env };
106
+ if (env.XDG_RUNTIME_DIR && !existsSync(env.XDG_RUNTIME_DIR)) delete env.XDG_RUNTIME_DIR;
107
+ return systemdUserBusEnvironment(env).XDG_RUNTIME_DIR || null;
108
+ }
109
+
110
+ /** Return explicit SSH user-bus discovery without changing the caller's environment. */
111
+ export function systemdUserBusEnvironment(environment: NodeJS.ProcessEnv = process.env, deps: {
112
+ uid?: number; exists?: (path: string) => boolean;
113
+ } = {}): NodeJS.ProcessEnv {
114
+ const env = { ...environment };
115
+ if (!env.XDG_RUNTIME_DIR) {
116
+ const uid = deps.uid ?? process.getuid?.();
117
+ const candidate = uid === undefined ? null : `/run/user/${uid}`;
118
+ if (candidate && (deps.exists ?? existsSync)(candidate)) env.XDG_RUNTIME_DIR = candidate;
110
119
  }
111
- return null;
120
+ return env;
112
121
  }
113
122
 
114
123
  /**
@@ -119,8 +128,8 @@ function userRuntimeDir(): string | null {
119
128
  */
120
129
  function ensureUserBusEnv(): void {
121
130
  if (process.env.XDG_RUNTIME_DIR) return;
122
- const dir = userRuntimeDir();
123
- if (dir) process.env.XDG_RUNTIME_DIR = dir;
131
+ const env = systemdUserBusEnvironment();
132
+ if (env.XDG_RUNTIME_DIR) process.env.XDG_RUNTIME_DIR = env.XDG_RUNTIME_DIR;
124
133
  }
125
134
 
126
135
  export function isSystemd(): boolean {
@@ -3,6 +3,8 @@ import { createHash } from "node:crypto";
3
3
  import { win32 } from "node:path";
4
4
  import { winswXmlPath } from "../lib/winsw";
5
5
  import { hardenSecretPath } from "../lib/windows-secret-acl";
6
+ import { durableBunRuntime, type DurableBunRuntime } from "../lib/bun-runtime";
7
+ import { assertSelectedRuntimeWritable, type RuntimePreflightDeps } from "../lib/bun-runtime-preflight";
6
8
  import { parseBakedListenPort } from "./health";
7
9
  import { windowsServiceScriptPath } from "./state";
8
10
  import { execFileSync } from "node:child_process";
@@ -18,7 +20,7 @@ import { killWindowsSchedulerWrappers } from "../lib/windows-service-wrappers";
18
20
  import { isTestHomeGuardArmed } from "../lib/test-home-guard";
19
21
  import { writeServiceApiTokenFile } from "./guards";
20
22
  import type { ServiceStopOutcome } from "./orchestration";
21
- import { TASK, windowsLauncherVbsPath, windowsTaskXmlPath, serviceStatePath, writeServiceInstallState, serviceSourceDir } from "./state";
23
+ import { TASK, windowsLauncherVbsPath, windowsTaskXmlPath, serviceStatePath, writeServiceInstallState, serviceSourceDir, cliEntry } from "./state";
22
24
  import { decodeSchtasksOutput, querySchtasks, schtasks, type WindowsSchedulerTaskProbe, schtasksErrorDetail, probeWindowsSchedulerTask, rollbackWindowsSchedulerTaskOwnedByAttempt } from "./windows-scheduler";
23
25
  import { buildWindowsServiceScript, buildWindowsSchtasksCreateArgs, buildWindowsSchtasksCreateArgsForXml, buildWindowsLauncherVbs, resolvedWindowsTaskSid, buildWindowsTaskXmlDocument, windowsTaskRegistrationOwnedByAttempt, windowsTaskRegistrationHealthy } from "./windows-taskxml";
24
26
 
@@ -108,11 +110,11 @@ function writeServiceAssetWithRetry(path: string, content: string, encoding: "ut
108
110
  * Rewrite on-disk scheduler assets (script/VBS/XML) without itself registering the task.
109
111
  * Fresh install creates it afterwards; repair does so only when the live definition is stale.
110
112
  */
111
- export function writeWindowsSchedulerAssets(): void {
113
+ export function writeWindowsSchedulerAssets(runtime: DurableBunRuntime = durableBunRuntime()): void {
112
114
  if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
113
115
  writeServiceApiTokenFile();
114
116
  const script = windowsServiceScriptPath();
115
- writeServiceAssetWithRetry(script, buildWindowsServiceScript(), "utf8");
117
+ writeServiceAssetWithRetry(script, buildWindowsServiceScript(cliEntry(runtime)), "utf8");
116
118
  // UTF-16LE + BOM: a BOM-less UTF-8 VBS mis-decodes non-ASCII (e.g. Korean) profile
117
119
  // paths on some WSH/codepage combinations — same contract as the task XML below.
118
120
  writeServiceAssetWithRetry(windowsLauncherVbsPath(), `\uFEFF${buildWindowsLauncherVbs(script)}`, "utf16le");
@@ -597,16 +599,40 @@ export function removeNativeWindowsServiceForScheduler(
597
599
  }
598
600
  }
599
601
 
600
- export function installWindows(): void {
601
- recordWindowsSchedulerOwnership();
602
- removeNativeWindowsServiceForScheduler();
602
+ /** External Windows effects shared by the gated entrypoints and admitted commit paths. */
603
+ export interface WindowsServiceInstallDeps extends RuntimePreflightDeps {
604
+ recordSchedulerOwnership?: () => boolean;
605
+ removeNativeService?: () => void;
606
+ stopScheduler?: () => void;
607
+ writeSchedulerAssets?: (runtime: DurableBunRuntime) => void;
608
+ schedulerCommand?: typeof schtasks;
609
+ writeState?: (backend: "scheduler" | "native", runtime: DurableBunRuntime) => void;
610
+ assertNativeAccount?: () => void;
611
+ prepareNativeConfig?: () => void;
612
+ uninstallScheduler?: () => void;
613
+ installNativeService?: typeof installWinswService;
614
+ }
615
+
616
+ export function installWindows(selected?: DurableBunRuntime, deps: WindowsServiceInstallDeps = {}): void {
617
+ const runtime = Object.freeze({ ...(selected ?? (deps.selectRuntime ?? durableBunRuntime)()) });
618
+ const configDir = (deps.configDir ?? getConfigDir)();
619
+ (deps.assertRuntimeWritable ?? assertSelectedRuntimeWritable)(runtime, configDir, { platform: deps.platform, rootWasAbsent: !existsSync(configDir) });
620
+ commitWindowsInstall(runtime, deps);
621
+ }
622
+
623
+ /** Internal commit boundary: the caller has admitted this frozen runtime before cleanup. */
624
+ export function commitWindowsInstall(runtime: DurableBunRuntime, deps: WindowsServiceInstallDeps = {}): void {
625
+ (deps.recordSchedulerOwnership ?? recordWindowsSchedulerOwnership)();
626
+ (deps.removeNativeService ?? removeNativeWindowsServiceForScheduler)();
603
627
  // End a running task BEFORE rewriting the assets it is executing — cmd.exe reading the
604
628
  // script mid-rewrite runs a torn batch file, and its open handle can fail the write.
605
- try { stopWindows(); } catch { /* not running */ }
606
- writeWindowsSchedulerAssets();
607
- schtasks(buildWindowsSchtasksCreateArgs(windowsServiceScriptPath()));
608
- schtasks(["/run", "/tn", TASK]);
609
- writeServiceInstallState("scheduler");
629
+ try { (deps.stopScheduler ?? stopWindows)(); } catch { /* not running */ }
630
+ (deps.writeSchedulerAssets ?? writeWindowsSchedulerAssets)(runtime);
631
+ const command = deps.schedulerCommand ?? schtasks;
632
+ command(buildWindowsSchtasksCreateArgs(windowsServiceScriptPath()));
633
+ command(["/run", "/tn", TASK]);
634
+ if (deps.writeState) deps.writeState("scheduler", runtime);
635
+ else writeServiceInstallState("scheduler", undefined, {}, runtime);
610
636
  }
611
637
 
612
638
  /**
@@ -748,26 +774,38 @@ function readWindowsPrincipalSource(): string | null {
748
774
  }
749
775
  }
750
776
 
751
- export async function installWindowsNative(): Promise<void> {
752
- assertWindowsNativeServiceAccountSupported();
753
- recordOwnedConfigPath(getConfigDir(), serviceStatePath());
754
- if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
755
- writeServiceApiTokenFile();
777
+ export async function installWindowsNative(selected?: DurableBunRuntime, deps: WindowsServiceInstallDeps = {}): Promise<void> {
778
+ const runtime = Object.freeze({ ...(selected ?? (deps.selectRuntime ?? durableBunRuntime)()) });
779
+ const configDir = (deps.configDir ?? getConfigDir)();
780
+ (deps.assertRuntimeWritable ?? assertSelectedRuntimeWritable)(runtime, configDir, { platform: deps.platform, rootWasAbsent: !existsSync(configDir) });
781
+ await commitWindowsNativeInstall(runtime, deps);
782
+ }
783
+
784
+ /** Internal commit boundary; direct native installs go through the public admission gate. */
785
+ export async function commitWindowsNativeInstall(runtime: DurableBunRuntime, deps: WindowsServiceInstallDeps = {}): Promise<void> {
786
+ (deps.assertNativeAccount ?? assertWindowsNativeServiceAccountSupported)();
787
+ if (deps.prepareNativeConfig) deps.prepareNativeConfig();
788
+ else {
789
+ recordOwnedConfigPath(getConfigDir(), serviceStatePath());
790
+ if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
791
+ writeServiceApiTokenFile();
792
+ }
793
+ const command = deps.schedulerCommand ?? schtasks;
756
794
  let hadScheduler = false;
757
795
  try {
758
- hadScheduler = schtasks(["/query", "/tn", TASK]).includes(TASK);
796
+ hadScheduler = command(["/query", "/tn", TASK]).includes(TASK);
759
797
  } catch { /* task absent */ }
760
798
  if (hadScheduler) {
761
799
  console.log("🔁 Removing the Task Scheduler backend before installing the native (WinSW) service...");
762
- try { stopWindows(); } catch { /* not running */ }
800
+ try { (deps.stopScheduler ?? stopWindows)(); } catch { /* not running */ }
763
801
  try {
764
- uninstallWindows();
802
+ (deps.uninstallScheduler ?? uninstallWindows)();
765
803
  } catch (err) {
766
804
  throw new Error(`Cannot remove the Task Scheduler backend before switching to native: ${err instanceof Error ? err.message : String(err)}`);
767
805
  }
768
806
  // Verify removal — schtasks /delete can silently fail if UAC or policy blocks it.
769
807
  try {
770
- if (schtasks(["/query", "/tn", TASK]).includes(TASK)) {
808
+ if (command(["/query", "/tn", TASK]).includes(TASK)) {
771
809
  throw new Error("Task Scheduler backend still present after removal — aborting switch.");
772
810
  }
773
811
  } catch (e) {
@@ -776,12 +814,13 @@ export async function installWindowsNative(): Promise<void> {
776
814
  }
777
815
  }
778
816
  try {
779
- await installWinswService(defaultWinswEntry(serviceSourceDir));
817
+ await (deps.installNativeService ?? installWinswService)(defaultWinswEntry(serviceSourceDir, runtime));
780
818
  } catch (err) {
781
819
  if (hadScheduler) console.error("⚠️ Native install failed AFTER removing the Task Scheduler backend — no service is installed now. Run `ocx service install` to restore the scheduler backend, or retry `--native`.");
782
820
  throw err;
783
821
  }
784
- writeServiceInstallState("native");
822
+ if (deps.writeState) deps.writeState("native", runtime);
823
+ else writeServiceInstallState("native", undefined, {}, runtime);
785
824
  }
786
825
 
787
826
  export function startWindows(): void { schtasks(["/run", "/tn", TASK]); }
@@ -0,0 +1,79 @@
1
+ import type { OcxConfig } from "../types";
2
+ import type { AnthropicInstanceId } from "../providers/anthropic-instance-id";
3
+ import { configuredAnthropicInstance } from "../providers/anthropic-instance";
4
+ import { anthropicRoutingFor } from "../oauth/anthropic-routing";
5
+ import { resolveAnthropicModelRouteForInstance, routeCandidates } from "../oauth/anthropic-model-routes";
6
+ import type { OAuthAccessSnapshot } from "../oauth";
7
+ import { AnthropicHelperUnavailableError } from "./auth";
8
+ import { captureAnthropicPhysicalSendOwnership, anthropicPhysicalSendOwnershipIsCurrent } from "../oauth/anthropic-send-ownership";
9
+ import { recordAnthropicAccountQuotaFromHeadersForInstance } from "../providers/quota/account-cache";
10
+ import { captureConfigGeneration } from "../lib/state-store-sweeper";
11
+ import { getAccountCredentialWithStatus } from "../oauth/store";
12
+ import { bindAnthropicRefusalCredentialForSend } from "../oauth/anthropic-account-refusal";
13
+
14
+ /** The one Messages URL builder shared by helper executors and the physical-send fence. */
15
+ export function anthropicHelperMessagesUrl(baseUrl: string): string {
16
+ return `${baseUrl.replace(/\/+$/, "").replace(/\/v1$/, "")}/v1/messages`;
17
+ }
18
+
19
+ /**
20
+ * Select the helper's account the way the legacy sidecar token path does: a read of the pool's
21
+ * selection for this model route, with no promotion. A helper never moves the active pointer and
22
+ * never consumes an operator's one-dispatch manual choice; the physical send is still fenced by
23
+ * the captured target/config, the account's credential generation and its send ownership.
24
+ */
25
+ export async function resolveAnthropicHelperSnapshot(config: OcxConfig, instance: AnthropicInstanceId, model: string): Promise<OAuthAccessSnapshot> {
26
+ const target = config.providers[instance]?.baseUrl;
27
+ const assertConfigured = () => {
28
+ const provider = config.providers[instance];
29
+ if (!provider || provider.disabled || provider.authMode !== "oauth" || provider.adapter !== "anthropic"
30
+ || configuredAnthropicInstance(config, instance) !== instance || provider.baseUrl !== target) {
31
+ throw new AnthropicHelperUnavailableError(instance);
32
+ }
33
+ };
34
+ assertConfigured();
35
+ const routing = anthropicRoutingFor(instance);
36
+ for (let attempt = 0; attempt < 3; attempt++) {
37
+ const route = resolveAnthropicModelRouteForInstance(instance, config, model);
38
+ if (route.error) throw new AnthropicHelperUnavailableError(instance);
39
+ const selection = routing.resolveAnthropicAccountForSession(null, config, Date.now(), route.decision, model);
40
+ if (!selection.accountId) throw new AnthropicHelperUnavailableError(instance);
41
+ const snapshot = await routing.getAnthropicPoolAccessSnapshot(selection.accountId);
42
+ assertConfigured();
43
+ const current = resolveAnthropicModelRouteForInstance(instance, config, model);
44
+ if (current.error || JSON.stringify(current.decision) !== JSON.stringify(route.decision)) continue;
45
+ if (snapshot.provider === instance && snapshot.accountId === selection.accountId) return snapshot;
46
+ }
47
+ throw new AnthropicHelperUnavailableError(instance);
48
+ }
49
+
50
+ /** One physical helper send; reset replays call this boundary again with the same snapshot. */
51
+ export async function fetchAnthropicHelper(
52
+ config: OcxConfig, snapshot: OAuthAccessSnapshot, model: string,
53
+ capturedTarget: string, url: string, init: RequestInit,
54
+ ): Promise<Response> {
55
+ const instance = snapshot.provider;
56
+ if (instance !== "anthropic" && instance !== "anthropic2") throw new Error("Invalid Anthropic helper instance");
57
+ const row = config.providers[instance];
58
+ const live = getAccountCredentialWithStatus(instance, snapshot.accountId);
59
+ if (!row || row.disabled || row.authMode !== "oauth" || row.adapter !== "anthropic"
60
+ || configuredAnthropicInstance(config, instance) !== instance || row.baseUrl !== capturedTarget
61
+ || url !== anthropicHelperMessagesUrl(capturedTarget) || !live || live.paused || live.needsReauth
62
+ || new Headers(init.headers).get("authorization") !== `Bearer ${snapshot.accessToken}`) {
63
+ throw new AnthropicHelperUnavailableError(instance);
64
+ }
65
+ const route = resolveAnthropicModelRouteForInstance(instance, config, model);
66
+ const routing = anthropicRoutingFor(instance);
67
+ if (route.error || !routeCandidates(routing.getEligibleAnthropicAccounts(Date.now(), model), route.decision).includes(snapshot.accountId)) {
68
+ throw new AnthropicHelperUnavailableError(instance);
69
+ }
70
+ const owner = captureAnthropicPhysicalSendOwnership(snapshot);
71
+ if (!owner) throw new AnthropicHelperUnavailableError(instance);
72
+ const writer = captureConfigGeneration();
73
+ const response = await fetch(url, init);
74
+ if (anthropicPhysicalSendOwnershipIsCurrent(owner)) {
75
+ recordAnthropicAccountQuotaFromHeadersForInstance(instance, snapshot.accountId, response.headers, writer, response.status, model);
76
+ bindAnthropicRefusalCredentialForSend(response, owner);
77
+ }
78
+ return response;
79
+ }
@@ -17,6 +17,73 @@ import { OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
17
17
  import { isCodexAccountUsable } from "../codex/account-usability";
18
18
  import { MAIN_CODEX_ACCOUNT_ID, isSelectableCodexPoolAccount } from "../codex/account-id";
19
19
  import { getAccountSet } from "../oauth/store";
20
+ import { configuredAnthropicInstance, type AnthropicInstanceId } from "../providers/anthropic-instance";
21
+ import { isAnthropicInstanceId } from "../providers/anthropic-instance-id";
22
+
23
+ /**
24
+ * The parent request's pool for an inheriting helper. A missing or disabled row keeps the parent's
25
+ * identity, so a pool removed mid-request refuses instead of discovering another pool; only a present
26
+ * custom row that is not the builtin instance (an unmarked custom `anthropic2`) stops inheritance.
27
+ */
28
+ export function inheritedAnthropicInstance(config: OcxConfig, parentProviderName: string | undefined): AnthropicInstanceId | undefined {
29
+ if (!isAnthropicInstanceId(parentProviderName)) return undefined;
30
+ const row = config.providers?.[parentProviderName];
31
+ if (!row || row.disabled === true) return parentProviderName;
32
+ return configuredAnthropicInstance(config, parentProviderName);
33
+ }
34
+
35
+ export class AnthropicHelperUnavailableError extends Error {
36
+ readonly code = "anthropic_helper_unavailable";
37
+ constructor(readonly instance: AnthropicInstanceId) {
38
+ super(`The selected ${instance} helper account pool is unavailable`);
39
+ this.name = "AnthropicHelperUnavailableError";
40
+ }
41
+ }
42
+ export type AnthropicHelperContext = {
43
+ backendFamily: string;
44
+ anthropicInstance?: AnthropicInstanceId;
45
+ parentProviderName?: string;
46
+ };
47
+
48
+ /**
49
+ * Availability of one selected pool: the configured builtin row plus at least one stored account the
50
+ * pool could select. Which account actually sends (model route, cooldown, manual choice) is decided
51
+ * by snapshot resolution at send time, exactly as the main request path does, so a paused or
52
+ * reauth-pending ACTIVE account does not make a pool with another usable account unavailable.
53
+ */
54
+ export function resolveAnthropicSidecarAuth(config: OcxConfig, instance: AnthropicInstanceId):
55
+ { anthropicProviderName: AnthropicInstanceId; anthropicProvider: OcxProviderConfig } | undefined {
56
+ const provider = config.providers[instance];
57
+ if (!provider || provider.disabled || provider.authMode !== "oauth" || provider.adapter !== "anthropic"
58
+ || configuredAnthropicInstance(config, instance) !== instance) return undefined;
59
+ if (!getAccountSet(instance)?.accounts.some(row => row.paused !== true && row.needsReauth !== true)) return undefined;
60
+ return { anthropicProviderName: instance, anthropicProvider: provider };
61
+ }
62
+
63
+ /**
64
+ * Explicit target, then the parent's CONFIGURED builtin instance. The parent name alone is not
65
+ * enough: a custom provider that happens to be named `anthropic2` is not Pool 2 and is never inherited.
66
+ */
67
+ export function resolveAnthropicHelperInstance(config: OcxConfig, context: AnthropicHelperContext): AnthropicInstanceId | undefined {
68
+ if (context.backendFamily !== "anthropic") return undefined;
69
+ const instance = context.anthropicInstance ?? inheritedAnthropicInstance(config, context.parentProviderName);
70
+ if (instance && !resolveAnthropicSidecarAuth(config, instance)) throw new AnthropicHelperUnavailableError(instance);
71
+ return instance;
72
+ }
73
+
74
+ /**
75
+ * Planner boundary for the typed refusal: an unavailable selected pool means "this helper does not
76
+ * run", never "fail the main request" and never "discover another pool". Other errors propagate.
77
+ */
78
+ export function withAnthropicHelperRefusal<T>(surface: string, resolve: () => T): T | undefined {
79
+ try {
80
+ return resolve();
81
+ } catch (error) {
82
+ if (!(error instanceof AnthropicHelperUnavailableError)) throw error;
83
+ console.warn(`[${surface}] ${error.message}; the request continues without this helper`);
84
+ return undefined;
85
+ }
86
+ }
20
87
 
21
88
  export interface SidecarAuthState {
22
89
  /** ChatGPT login usable: canonical forward provider AND a live stored credential. */
@@ -63,6 +130,10 @@ function findAnthropicAuthProvider(
63
130
  config: OcxConfig,
64
131
  ): { providerName: string; provider: OcxProviderConfig } | undefined {
65
132
  for (const [providerName, provider] of Object.entries(config.providers)) {
133
+ // Never adds Pool 2 automatically. The OAuth store key `anthropic2` belongs to the builtin pool:
134
+ // its token path and the token guardian refuse an unmarked row of that name, so offering such a
135
+ // custom row here would advertise credentials no helper executor will spend.
136
+ if (providerName === "anthropic2") continue;
66
137
  if (provider.disabled === true) continue;
67
138
  if (provider.adapter !== "anthropic" || provider.authMode !== "oauth") continue;
68
139
  const set = getAccountSet(providerName);
@@ -72,8 +143,11 @@ function findAnthropicAuthProvider(
72
143
  return undefined;
73
144
  }
74
145
 
75
- export function resolveSidecarAuth(config: OcxConfig): SidecarAuthState {
76
- const anthropic = findAnthropicAuthProvider(config);
146
+ export function resolveSidecarAuth(config: OcxConfig, instance?: AnthropicInstanceId): SidecarAuthState {
147
+ const exact = instance ? resolveAnthropicSidecarAuth(config, instance) : undefined;
148
+ if (instance && !exact) throw new AnthropicHelperUnavailableError(instance);
149
+ const anthropic = exact ? { providerName: exact.anthropicProviderName, provider: exact.anthropicProvider }
150
+ : findAnthropicAuthProvider(config);
77
151
  return {
78
152
  isCodexAuth: hasUsableCodexLogin(config),
79
153
  isAnthropicAuth: anthropic !== undefined,
@@ -65,6 +65,8 @@ export interface PolicyJobTestHooks {
65
65
  * policy load, so concurrent PUTs can race completion metadata writes.
66
66
  */
67
67
  blockMs?: number;
68
+ /** Test-only shared barrier after the Worker has loaded its initial policy. */
69
+ workerLoadGate?: Int32Array;
68
70
  /**
69
71
  * When true, run on the main thread via `queueMicrotask` + optional sleep.
70
72
  * Used only for unit tests that cannot spawn workers; responsiveness tests
@@ -304,7 +306,7 @@ function applyMutationBusy(): void {
304
306
  };
305
307
  }
306
308
 
307
- function runInWorker(opts: RequestPolicyRunOptions & { blockMs?: number }): Promise<PolicyRunResult> {
309
+ function runInWorker(opts: RequestPolicyRunOptions & { blockMs?: number; workerLoadGate?: Int32Array }): Promise<PolicyRunResult> {
308
310
  const reservation = tryReserveStorageWorker();
309
311
  if (!reservation) return Promise.reject(new StorageWorkerAdmissionBusyError());
310
312
  return withStorageWorkerSpawnGate(() => new Promise<PolicyRunResult>((resolve, reject) => {
@@ -369,6 +371,7 @@ function runInWorker(opts: RequestPolicyRunOptions & { blockMs?: number }): Prom
369
371
  ...(opts.codexHome ? { codexHome: opts.codexHome } : {}),
370
372
  ...(opts.busyTimeoutMs !== undefined ? { busyTimeoutMs: opts.busyTimeoutMs } : {}),
371
373
  ...(opts.blockMs !== undefined ? { blockMs: opts.blockMs } : {}),
374
+ ...(opts.workerLoadGate ? { workerLoadGate: opts.workerLoadGate } : {}),
372
375
  env: {
373
376
  ...(process.env.CODEX_HOME ? { CODEX_HOME: process.env.CODEX_HOME } : {}),
374
377
  ...(process.env.OPENCODEX_HOME ? { OPENCODEX_HOME: process.env.OPENCODEX_HOME } : {}),
@@ -410,6 +413,7 @@ async function executeJob(opts: RequestPolicyRunOptions): Promise<void> {
410
413
  ...opts,
411
414
  codexHome,
412
415
  ...(typeof blockMs === "number" && blockMs > 0 ? { blockMs } : {}),
416
+ ...(testHooks?.workerLoadGate ? { workerLoadGate: testHooks.workerLoadGate } : {}),
413
417
  });
414
418
  }
415
419
 
@@ -13,6 +13,7 @@ interface RunMessage {
13
13
  busyTimeoutMs?: number;
14
14
  /** Test-only: block after loading the start-of-job policy (see holdAfterLoadMs). */
15
15
  blockMs?: number;
16
+ workerLoadGate?: Int32Array;
16
17
  /** Env snapshot — Workers may not see parent mutations on all platforms. */
17
18
  env?: { CODEX_HOME?: string; OPENCODEX_HOME?: string };
18
19
  }
@@ -29,7 +30,7 @@ declare const self: Worker;
29
30
 
30
31
  self.onmessage = (event: MessageEvent<unknown>) => {
31
32
  if (!isRunMessage(event.data)) return;
32
- const { requestId, reason, force, codexHome, busyTimeoutMs, blockMs, env } = event.data;
33
+ const { requestId, reason, force, codexHome, busyTimeoutMs, blockMs, workerLoadGate, env } = event.data;
33
34
  try {
34
35
  if (env?.CODEX_HOME) process.env.CODEX_HOME = env.CODEX_HOME;
35
36
  if (env?.OPENCODEX_HOME) process.env.OPENCODEX_HOME = env.OPENCODEX_HOME;
@@ -38,6 +39,7 @@ self.onmessage = (event: MessageEvent<unknown>) => {
38
39
  force: force === true,
39
40
  ...(codexHome ? { codexHome } : {}),
40
41
  ...(busyTimeoutMs !== undefined ? { busyTimeoutMs } : {}),
42
+ ...(workerLoadGate ? { holdAfterLoadGate: workerLoadGate } : {}),
41
43
  ...(typeof blockMs === "number" && Number.isFinite(blockMs) && blockMs > 0
42
44
  ? { holdAfterLoadMs: Math.floor(blockMs) }
43
45
  : {}),
@@ -74,6 +74,8 @@ export interface PolicyRunDeps {
74
74
  * land before completion merges run metadata.
75
75
  */
76
76
  holdAfterLoadMs?: number;
77
+ /** Test-only Worker barrier: 0=pending, 1=policy loaded, 2=released. */
78
+ holdAfterLoadGate?: Int32Array;
77
79
  }
78
80
 
79
81
  /** Canonical defaults — enabled is always false. */
@@ -500,6 +502,13 @@ export function runStorageCleanupPolicy(deps: PolicyRunDeps): PolicyRunResult {
500
502
  })
501
503
  : (patch: PolicyRunMetadataPatch) => commitPolicyRunMetadataToConfig(patch, policy);
502
504
 
505
+ if (deps.holdAfterLoadGate) {
506
+ Atomics.compareExchange(deps.holdAfterLoadGate, 0, 0, 1);
507
+ Atomics.notify(deps.holdAfterLoadGate, 0);
508
+ if (Atomics.wait(deps.holdAfterLoadGate, 0, 1, 10_000) === "timed-out") {
509
+ throw new Error("policy_test_load_gate_timeout");
510
+ }
511
+ }
503
512
  if (typeof deps.holdAfterLoadMs === "number" && Number.isFinite(deps.holdAfterLoadMs) && deps.holdAfterLoadMs > 0) {
504
513
  Bun.sleepSync(Math.floor(deps.holdAfterLoadMs));
505
514
  }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Anthropic OAuth account-pool settings. One shape, two locations:
3
+ * `config.anthropicAccountPool` for the `anthropic` instance and
4
+ * `config.providers.anthropic2.anthropicAccountPool` for `anthropic2`. Neither inherits from the other.
5
+ *
6
+ * Leaf module: `config.ts` and `provider.ts` import these types, never the reverse.
7
+ */
8
+ export type OcxAccountPoolRotationStrategy = "quota" | "round-robin" | "fill-first";
9
+
10
+ export type OcxAccountPoolQuotaWindow = "five-hour" | "weekly" | "max-utilization";
11
+
12
+ export interface AnthropicModelRoute {
13
+ name: string;
14
+ match: string;
15
+ accounts: string[];
16
+ fallback?: boolean;
17
+ }
18
+
19
+ export interface AnthropicAccountPoolConfig {
20
+ enabled?: boolean;
21
+ /** Preserve native Claude Messages while the pool is enabled. Default true; false selects legacy translation. */
22
+ nativeMessages?: boolean;
23
+ /** Usage % threshold for new-session auto-pick. Default 80. 0 = disabled (affinity/active only). */
24
+ autoSwitchThreshold?: number;
25
+ /** New-session rotation strategy. Default quota (today's behaviour). */
26
+ strategy?: OcxAccountPoolRotationStrategy;
27
+ /** Successful new-session binds retained on one round-robin selection. Default 1; range 1..100. */
28
+ stickyLimit?: number;
29
+ /** Usage window for quota-based scoring. Default "five-hour" (today's behaviour). */
30
+ quotaWindow?: OcxAccountPoolQuotaWindow;
31
+ /** Ordered model allowlists; inactive while the pool is disabled. Stored account IDs only. */
32
+ routes?: AnthropicModelRoute[];
33
+ }