@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
@@ -14,7 +14,11 @@
14
14
  import { jsonResponse } from "../auth-cors";
15
15
  import type { ManagementContext } from "./context";
16
16
  import { getValidAccessSnapshotForAccount } from "../../oauth";
17
- import { captureOAuthAccountSelection, listAccounts } from "../../oauth/store";
17
+ import { configuredAnthropicInstance, isAnthropicInstanceId, type AnthropicInstanceId } from "../../providers/anthropic-instance";
18
+ import { captureAnthropicPhysicalSendOwnership, anthropicPhysicalSendOwnershipIsCurrent } from "../../oauth/anthropic-send-ownership";
19
+ import { anthropicCooldownRecoveryFor } from "../../providers/quota/anthropic-cooldown-recovery";
20
+ import { captureProviderAccountQuotaEpoch } from "../../providers/quota/account-cache";
21
+ import { captureOAuthAccountSelection, getAccountSet, getAccountCredentialWithStatus, listAccounts } from "../../oauth/store";
18
22
  import {
19
23
  ANTHROPIC_RESET_GRANT_ID_RE,
20
24
  AnthropicResetGrantError,
@@ -28,19 +32,20 @@ import {
28
32
  AnthropicResetLedgerError,
29
33
  anthropicOrgDigest,
30
34
  anthropicResetOperationExists,
35
+ anthropicResetJournalPathForInstance,
31
36
  beginAnthropicResetOperation,
32
37
  pendingAnthropicResetOperation,
33
38
  releaseAnthropicResetLease,
34
39
  settleAnthropicResetOperation,
35
40
  } from "../../providers/anthropic-reset-grant-ledger";
36
41
 
37
- const PROVIDER = "anthropic";
38
42
  const UUID_V4_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
39
43
 
40
44
  const MESSAGES = {
41
45
  method_not_allowed: "Method not allowed",
42
46
  session_required: "Spending a reset requires a dashboard session.",
43
47
  invalid_json: "Request body must be JSON.",
48
+ invalid_provider: "No matching builtin Anthropic OAuth provider.",
44
49
  invalid_grant_id: "grantId is missing or malformed.",
45
50
  invalid_operation_id: "operationId must be a UUIDv4.",
46
51
  no_account: "No matching Anthropic OAuth account.",
@@ -60,6 +65,9 @@ const MESSAGES = {
60
65
  type ErrorCode = keyof typeof MESSAGES;
61
66
 
62
67
  export interface AnthropicResetGrantRouteDeps {
68
+ provider?: AnthropicInstanceId;
69
+ isCurrent?: () => boolean;
70
+ forProvider?: (provider: AnthropicInstanceId) => AnthropicResetGrantRouteDeps;
63
71
  fetchFn?: typeof globalThis.fetch;
64
72
  journalPath?: string;
65
73
  now?: () => number;
@@ -68,25 +76,64 @@ export interface AnthropicResetGrantRouteDeps {
68
76
  accessTokenFor: (accountId: string) => Promise<string>;
69
77
  }
70
78
 
71
- const defaultDeps: AnthropicResetGrantRouteDeps = {
72
- listAccountIds: () => listAccounts(PROVIDER).map(account => account.id),
73
- activeAccountId: () => captureOAuthAccountSelection(PROVIDER)?.accountId ?? null,
74
- accessTokenFor: async accountId =>
75
- (await getValidAccessSnapshotForAccount(PROVIDER, accountId, { requireUsableAccount: true })).accessToken,
76
- };
79
+ function defaultDepsFor(ctx: ManagementContext, provider: AnthropicInstanceId): AnthropicResetGrantRouteDeps | null {
80
+ // One source: the live management config. A keeps its legacy reach without a providers row;
81
+ // B requires its configured builtin row. A fresh file load here would refuse on any drift.
82
+ const live = ctx.config;
83
+ const target = live.providers?.[provider];
84
+ if (target?.disabled || target && target.authMode !== "oauth" || configuredAnthropicInstance(live, provider) !== provider) return null;
85
+ const identity = JSON.stringify(target ?? null);
86
+ let owner: ReturnType<typeof captureAnthropicPhysicalSendOwnership> = null;
87
+ const targetCurrent = () => configuredAnthropicInstance(live, provider) === provider
88
+ && JSON.stringify(live.providers?.[provider] ?? null) === identity;
89
+ return {
90
+ // Only the journal path: the ledger's numeric `now` option is not the deps clock function.
91
+ provider, journalPath: anthropicResetJournalPathForInstance(provider),
92
+ listAccountIds: () => listAccounts(provider).map(account => account.id),
93
+ activeAccountId: () => captureOAuthAccountSelection(provider)?.accountId ?? null,
94
+ isCurrent: () => {
95
+ if (!targetCurrent() || !owner || !anthropicPhysicalSendOwnershipIsCurrent(owner)) return false;
96
+ const row = getAccountCredentialWithStatus(provider, owner.accountId);
97
+ return !!row && !row.paused && !row.needsReauth;
98
+ },
99
+ accessTokenFor: async accountId => {
100
+ const initial = getAccountSet(provider)?.accounts.find(row => row.id === accountId);
101
+ const recovery = anthropicCooldownRecoveryFor(provider);
102
+ const incarnation = recovery.reserveAnthropicAccountIncarnation(accountId);
103
+ const epoch = captureProviderAccountQuotaEpoch(provider);
104
+ const snapshot = await getValidAccessSnapshotForAccount(provider, accountId, { requireUsableAccount: true });
105
+ const current = getAccountSet(provider)?.accounts.find(row => row.id === accountId);
106
+ if (!initial || !current || initial.loginId !== current.loginId || initial.addedAt !== current.addedAt
107
+ || recovery.anthropicAccountIncarnation(accountId) !== incarnation || captureProviderAccountQuotaEpoch(provider) !== epoch) throw new Error("reset account replaced");
108
+ owner = captureAnthropicPhysicalSendOwnership(snapshot);
109
+ if (!targetCurrent() || !owner) throw new Error("reset owner changed");
110
+ return snapshot.accessToken;
111
+ },
112
+ };
113
+ }
114
+
115
+ function selectDeps(ctx: ManagementContext, requested: unknown, deps?: AnthropicResetGrantRouteDeps): AnthropicResetGrantRouteDeps | null {
116
+ const provider = requested === undefined || requested === null ? "anthropic" : requested;
117
+ if (!isAnthropicInstanceId(provider)) return null;
118
+ const selected = deps?.forProvider?.(provider) ?? deps;
119
+ // Legacy injected dependencies are A-only; B must explicitly bind its own journal.
120
+ if (selected) return (selected.provider ?? "anthropic") === provider
121
+ ? { journalPath: anthropicResetJournalPathForInstance(provider), ...selected, provider } : null;
122
+ return defaultDepsFor(ctx, provider);
123
+ }
77
124
 
78
125
  function fail(ctx: ManagementContext, status: number, code: ErrorCode, extra: Record<string, unknown> = {}): Response {
79
126
  return jsonResponse({ error: { code, message: MESSAGES[code], ...extra } }, status, ctx.req, ctx.config);
80
127
  }
81
128
 
82
- function ledgerFailure(ctx: ManagementContext, error: unknown): Response {
83
- if (error instanceof AnthropicResetLedgerError && error.code === "busy") return fail(ctx, 503, "ledger_busy");
84
- return fail(ctx, 503, "ledger_unavailable");
129
+ function ledgerFailure(ctx: ManagementContext, error: unknown, extra: Record<string, unknown> = {}): Response {
130
+ if (error instanceof AnthropicResetLedgerError && error.code === "busy") return fail(ctx, 503, "ledger_busy", extra);
131
+ return fail(ctx, 503, "ledger_unavailable", extra);
85
132
  }
86
133
 
87
- function readFailure(ctx: ManagementContext, error: unknown): Response {
88
- if (error instanceof AnthropicResetGrantError && error.code === "auth") return fail(ctx, 401, "auth_failed");
89
- return fail(ctx, 502, "upstream_unavailable");
134
+ function readFailure(ctx: ManagementContext, error: unknown, extra: Record<string, unknown> = {}): Response {
135
+ if (error instanceof AnthropicResetGrantError && error.code === "auth") return fail(ctx, 401, "auth_failed", extra);
136
+ return fail(ctx, 502, "upstream_unavailable", extra);
90
137
  }
91
138
 
92
139
  function resolveAccountId(deps: AnthropicResetGrantRouteDeps, requested: unknown): string | null {
@@ -107,16 +154,19 @@ async function tokenFor(deps: AnthropicResetGrantRouteDeps, accountId: string):
107
154
  }
108
155
 
109
156
  async function handleRead(ctx: ManagementContext, deps: AnthropicResetGrantRouteDeps): Promise<Response> {
157
+ const receipt = { provider: deps.provider };
110
158
  const accountId = resolveAccountId(deps, ctx.url.searchParams.get("accountId") ?? undefined);
111
- if (!accountId) return fail(ctx, 400, "no_account");
159
+ if (!accountId) return fail(ctx, 400, "no_account", receipt);
112
160
  const accessToken = await tokenFor(deps, accountId);
113
- if (!accessToken) return fail(ctx, 401, "auth_failed");
161
+ if (!accessToken) return fail(ctx, 401, "auth_failed", receipt);
162
+ if (deps.isCurrent?.() === false) return fail(ctx, 401, "auth_failed", receipt);
114
163
  let status;
115
164
  try {
116
165
  status = await fetchAnthropicResetGrantStatus({ accessToken, fetchFn: deps.fetchFn });
117
166
  } catch (error) {
118
- return readFailure(ctx, error);
167
+ return readFailure(ctx, error, receipt);
119
168
  }
169
+ if (deps.isCurrent?.() === false) return fail(ctx, 401, "auth_failed", receipt);
120
170
  let pendingOperation = null;
121
171
  let journalAvailable = true;
122
172
  try {
@@ -124,10 +174,10 @@ async function handleRead(ctx: ManagementContext, deps: AnthropicResetGrantRoute
124
174
  } catch {
125
175
  journalAvailable = false;
126
176
  }
127
- return jsonResponse({ accountId, ...status, pendingOperation, journalAvailable }, 200, ctx.req, ctx.config);
177
+ return jsonResponse({ provider: deps.provider, accountId, ...status, pendingOperation, journalAvailable }, 200, ctx.req, ctx.config);
128
178
  }
129
179
 
130
- async function handleConsume(ctx: ManagementContext, deps: AnthropicResetGrantRouteDeps): Promise<Response> {
180
+ async function handleConsume(ctx: ManagementContext, injected?: AnthropicResetGrantRouteDeps): Promise<Response> {
131
181
  if (ctx.principal !== "gui-session") return fail(ctx, 403, "session_required");
132
182
  let body: Record<string, unknown>;
133
183
  try {
@@ -137,21 +187,27 @@ async function handleConsume(ctx: ManagementContext, deps: AnthropicResetGrantRo
137
187
  } catch {
138
188
  return fail(ctx, 400, "invalid_json");
139
189
  }
190
+ const deps = selectDeps(ctx, body.provider, injected);
191
+ if (!deps) return fail(ctx, 400, "invalid_provider");
192
+ const provider = deps.provider;
193
+ const operationReceipt = { provider };
194
+ const scopedFail = (status: number, code: ErrorCode, extra: Record<string, unknown> = {}) => fail(ctx, status, code, { ...operationReceipt, ...extra });
140
195
  const { grantId, operationId } = body;
141
- if (typeof grantId !== "string" || !ANTHROPIC_RESET_GRANT_ID_RE.test(grantId)) return fail(ctx, 400, "invalid_grant_id");
142
- if (typeof operationId !== "string" || !UUID_V4_RE.test(operationId)) return fail(ctx, 400, "invalid_operation_id");
143
- if (typeof body.accountId !== "string") return fail(ctx, 400, "no_account");
196
+ if (typeof grantId !== "string" || !ANTHROPIC_RESET_GRANT_ID_RE.test(grantId)) return scopedFail(400, "invalid_grant_id");
197
+ if (typeof operationId !== "string" || !UUID_V4_RE.test(operationId)) return scopedFail(400, "invalid_operation_id");
198
+ if (typeof body.accountId !== "string") return scopedFail(400, "no_account");
144
199
  const accountId = resolveAccountId(deps, body.accountId);
145
- if (!accountId) return fail(ctx, 400, "no_account");
200
+ if (!accountId) return scopedFail(400, "no_account");
146
201
 
147
202
  const accessToken = await tokenFor(deps, accountId);
148
- if (!accessToken) return fail(ctx, 401, "auth_failed");
203
+ if (!accessToken || deps.isCurrent?.() === false) return scopedFail(401, "auth_failed");
149
204
  let organizationUuid: string;
150
205
  try {
151
206
  organizationUuid = await fetchAnthropicOrganizationUuid({ accessToken, fetchFn: deps.fetchFn });
152
207
  } catch (error) {
153
- return readFailure(ctx, error);
208
+ return readFailure(ctx, error, operationReceipt);
154
209
  }
210
+ if (deps.isCurrent?.() === false) return scopedFail(401, "auth_failed");
155
211
  const ledger = { journalPath: deps.journalPath, now: deps.now?.() };
156
212
 
157
213
  // A new operation must pass the spend gate on a fresh read. A same-id retry of
@@ -161,39 +217,40 @@ async function handleConsume(ctx: ManagementContext, deps: AnthropicResetGrantRo
161
217
  try {
162
218
  known = anthropicResetOperationExists(operationId, ledger);
163
219
  } catch (error) {
164
- return ledgerFailure(ctx, error);
220
+ return ledgerFailure(ctx, error, operationReceipt);
165
221
  }
166
222
  if (!known) {
167
223
  let status;
168
224
  try {
169
225
  status = await fetchAnthropicResetGrantStatus({ accessToken, fetchFn: deps.fetchFn });
170
226
  } catch (error) {
171
- return readFailure(ctx, error);
227
+ return readFailure(ctx, error, operationReceipt);
172
228
  }
173
229
  const blocker = anthropicResetGrantBlocker(status, grantId);
174
- if (blocker) return fail(ctx, 409, "grant_not_usable", { reason: blocker });
230
+ if (blocker) return scopedFail(409, "grant_not_usable", { reason: blocker });
175
231
  }
176
232
 
177
233
  let begin;
234
+ if (deps.isCurrent?.() === false) return scopedFail(401, "auth_failed");
178
235
  try {
179
236
  begin = beginAnthropicResetOperation(
180
237
  { operationId, accountId, grantId, orgDigest: anthropicOrgDigest(organizationUuid) },
181
238
  { journalPath: deps.journalPath, now: deps.now?.() },
182
239
  );
183
240
  } catch (error) {
184
- return ledgerFailure(ctx, error);
241
+ return ledgerFailure(ctx, error, operationReceipt);
185
242
  }
186
243
  switch (begin.kind) {
187
244
  case "replay":
188
245
  return jsonResponse(
189
- { code: begin.code, replayed: true, resetsLeft: begin.resetsLeft, accountId, grantId, operationId },
246
+ { provider, code: begin.code, replayed: true, resetsLeft: begin.resetsLeft, accountId, grantId, operationId },
190
247
  200, ctx.req, ctx.config,
191
248
  );
192
- case "identity-mismatch": return fail(ctx, 409, "operation_identity_mismatch");
193
- case "in-flight": return fail(ctx, 409, "in_flight");
194
- case "expired": return fail(ctx, 409, "unknown_outcome_expired");
195
- case "unresolved-prior": return fail(ctx, 409, "unresolved_prior_operation", { pendingOperationId: begin.operationId });
196
- case "capacity": return fail(ctx, 503, "ledger_capacity");
249
+ case "identity-mismatch": return scopedFail(409, "operation_identity_mismatch");
250
+ case "in-flight": return scopedFail(409, "in_flight");
251
+ case "expired": return scopedFail(409, "unknown_outcome_expired");
252
+ case "unresolved-prior": return scopedFail(409, "unresolved_prior_operation", { pendingOperationId: begin.operationId });
253
+ case "capacity": return scopedFail(503, "ledger_capacity");
197
254
  case "execute": break;
198
255
  }
199
256
 
@@ -206,7 +263,7 @@ async function handleConsume(ctx: ManagementContext, deps: AnthropicResetGrantRo
206
263
  try {
207
264
  releaseAnthropicResetLease(operationId, { journalPath: deps.journalPath, now: deps.now?.() });
208
265
  } catch { /* the lease expires on its own */ }
209
- return fail(ctx, 502, "unknown_outcome", { operationId, grantId });
266
+ return scopedFail(502, "unknown_outcome", { operationId, grantId, accountId });
210
267
  }
211
268
  let settled;
212
269
  try {
@@ -216,13 +273,14 @@ async function handleConsume(ctx: ManagementContext, deps: AnthropicResetGrantRo
216
273
  );
217
274
  } catch {
218
275
  // Fail closed: the caller must not read this as a durable result.
219
- return fail(ctx, 500, "journal_write_failed", { operationId, grantId });
276
+ return scopedFail(500, "journal_write_failed", { operationId, grantId, accountId });
220
277
  }
221
278
  // Report what the journal holds. An overlapping same-id attempt may have
222
279
  // settled first; its answer is the canonical one.
223
280
  const replayed = settled.code !== answer.code || settled.resetsLeft !== answer.resetsLeft;
224
281
  return jsonResponse(
225
282
  {
283
+ provider,
226
284
  code: settled.code,
227
285
  replayed,
228
286
  resetsLeft: settled.resetsLeft,
@@ -237,12 +295,13 @@ async function handleConsume(ctx: ManagementContext, deps: AnthropicResetGrantRo
237
295
 
238
296
  export async function handleAnthropicResetGrantRoutes(
239
297
  ctx: ManagementContext,
240
- deps: AnthropicResetGrantRouteDeps = defaultDeps,
298
+ deps?: AnthropicResetGrantRouteDeps,
241
299
  ): Promise<Response | null> {
242
300
  const { pathname } = ctx.url;
243
301
  if (pathname === "/api/anthropic/reset-grants") {
244
302
  if (ctx.req.method !== "GET") return fail(ctx, 405, "method_not_allowed");
245
- return handleRead(ctx, deps);
303
+ const selected = selectDeps(ctx, ctx.url.searchParams.get("provider"), deps);
304
+ return selected ? handleRead(ctx, selected) : fail(ctx, 400, "invalid_provider");
246
305
  }
247
306
  if (pathname === "/api/anthropic/reset-grants/consume") {
248
307
  if (ctx.req.method !== "POST") return fail(ctx, 405, "method_not_allowed");
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Per-role model for omo (Codex / LazyCodex): `$CODEX_HOME/agents/<role>.toml` plus LazyCodex's
3
- * `codex.agents.<role>.model` mirror in omo.jsonc. Both halves exist only when LazyCodex is
3
+ * `"[codex]".agents.<role>` model and reasoning mirror in omo.jsonc. Both halves exist only when LazyCodex is
4
4
  * detected; Pi-based and OpenCode-based omo are never read here.
5
5
  *
6
6
  * Loaded on demand from `src/server/management-api.ts`, like the quota-reset handler, so a
@@ -40,6 +40,7 @@ export async function handleCodexAgentRoleRoutes(ctx: ManagementContext): Promis
40
40
  omoJsonc: { state: omoState.state },
41
41
  roles: roles.listCodexAgentRoleModels(codexHome).map(entry => ({
42
42
  ...entry,
43
+ effort: roles.readCodexAgentRoleEffort(entry.role, codexHome),
43
44
  omoJsoncModel: omoModels[entry.role] ?? null,
44
45
  })),
45
46
  }, 200, req, config);
@@ -114,7 +115,8 @@ export async function handleCodexAgentRoleRoutes(ctx: ManagementContext): Promis
114
115
  // The role TOML is what Codex obeys, so its write stands even when the omo mirror cannot follow.
115
116
  let omoStatus: ReturnType<typeof omo.writeOmoRoleModel> | "write_failed";
116
117
  try {
117
- omoStatus = omo.writeOmoRoleModel(role, model, omo.omoJsoncPath());
118
+ const reasoning = effort === undefined ? undefined : omo.omoReasoningFor(effort);
119
+ omoStatus = omo.writeOmoRoleModel(role, model, omo.omoJsoncPath(), reasoning);
118
120
  } catch {
119
121
  omoStatus = "write_failed";
120
122
  }
@@ -83,6 +83,7 @@ const WRITE_ERROR_STATUS: Record<WriteError, number> = {
83
83
  write_failed: 500,
84
84
  recovery_required: 409,
85
85
  locked: 409,
86
+ unsafe: 422,
86
87
  };
87
88
 
88
89
  /** Read-only view for the route test that asserts every mapping is a client error. */
@@ -58,6 +58,10 @@ async function modelCandidates(slugs: readonly string[], models: readonly Catalo
58
58
  ]);
59
59
  const nativeSlugs = catalog.listCatalogNativeSlugs();
60
60
  const routed = new Map(models.map(model => [catalog.catalogModelSlug(model), model]));
61
+ // A routed model that declares no levels still gets the ladder Codex shows for it, the same one
62
+ // the Models page lists, so its proposal carries an effort instead of none. An explicit empty
63
+ // ladder means the model takes no effort control, and the written catalog lists none for it.
64
+ const written = catalog.catalogModelLadders(slugs);
61
65
  const unitPrice = (provider: string, modelId: string): number | null => {
62
66
  const cost = resolveMatchedPrice(provider, modelId)?.cost4;
63
67
  return cost ? cost.input + cost.output : null;
@@ -65,11 +69,18 @@ async function modelCandidates(slugs: readonly string[], models: readonly Catalo
65
69
  return slugs.map(slug => {
66
70
  const row = routed.get(slug);
67
71
  if (row) {
72
+ const ladder = written.get(slug);
73
+ const efforts = Array.isArray(row.reasoningEfforts) ? row.reasoningEfforts : ladder?.efforts ?? [];
74
+ // A row without its own default keeps the one Codex shows, when that rung is on the ladder.
75
+ const shownDefault = ladder?.defaultEffort !== undefined && efforts.includes(ladder.defaultEffort)
76
+ ? ladder.defaultEffort
77
+ : undefined;
78
+ const defaultEffort = row.defaultReasoningEffort ?? shownDefault;
68
79
  return {
69
80
  model: slug,
70
81
  unitPrice: unitPrice(row.provider, row.id),
71
- efforts: row.reasoningEfforts ?? [],
72
- ...(row.defaultReasoningEffort ? { defaultEffort: row.defaultReasoningEffort } : {}),
82
+ efforts,
83
+ ...(defaultEffort ? { defaultEffort } : {}),
73
84
  };
74
85
  }
75
86
  if (nativeSlugs.includes(slug)) {
@@ -4,9 +4,12 @@ import { getMainAccountExternalUsageWarning } from "../../codex/main-account-ext
4
4
  import { getObservedMainQuotaIdentityKey } from "../../codex/main-account-cache";
5
5
  import { compactionRoutingSchema, memoryModelsSchema } from "../../config/schema/leaf-validators";
6
6
  import { compactionRecoverySchema } from "../../config/schema/compaction-recovery";
7
+ import { anthropicSidecarPatchError } from "../../config/schema/anthropic-account-pool";
8
+ import { isAnthropicInstanceId } from "../../providers/anthropic-instance-id";
7
9
  import { captureConfigTopLevelRollback } from "../../config/rebase-provenance";
8
10
  import type { IntegrationClientId } from "../../integrations/registry";
9
11
  import { randomUUID } from "node:crypto";
12
+ import { isDeepStrictEqual } from "node:util";
10
13
  import { readFileSync } from "node:fs";
11
14
  import type { CatalogModel } from "../../codex/catalog";
12
15
  import { catalogModelSlug, invalidateCodexModelsCache, nativeContextLimits, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
@@ -85,7 +88,6 @@ import { getProviderRegistryEntry } from "../../providers/registry";
85
88
  import { VISION_REASONING_EFFORTS, isVisionReasoningEffort } from "../../reasoning-effort";
86
89
  import { normalizeVisionReasoningForModel } from "../../vision/reasoning";
87
90
  import {
88
- findAnthropicVisionProvider,
89
91
  isValidVisionTimeoutMs,
90
92
  MAX_VISION_TIMEOUT_MS,
91
93
  MIN_VISION_TIMEOUT_MS,
@@ -105,6 +107,7 @@ import {
105
107
  webSearchModelIsRejected,
106
108
  webSearchModelOptionsFrom,
107
109
  webSearchModelRejection,
110
+ sidecarAnthropicPoolOptions, sidecarOptionsAuth, sidecarSettingsAfterPatch,
108
111
  } from "./web-search-sidecar-options";
109
112
  import { validateXaiSearchOptions } from "../../web-search/xai-executor";
110
113
  import { getDebugLogEntries } from "../../lib/debug-log-buffer";
@@ -150,7 +153,9 @@ async function sidecarVisionResponseSettings(config: OcxConfig): Promise<{
150
153
  const vs = config.visionSidecar ?? {};
151
154
  // Match the runtime's one selected Anthropic executor for both backend fallback
152
155
  // and catalog reachability; resolving it once prevents the two projections drifting.
153
- const anthropicSidecar = findAnthropicVisionProvider(config);
156
+ const auth = sidecarOptionsAuth(config, vs.anthropicInstance);
157
+ const anthropicSidecar = auth.isAnthropicAuth && auth.anthropicProviderName && auth.anthropicProvider
158
+ ? { providerName: auth.anthropicProviderName, provider: auth.anthropicProvider, config } : undefined;
154
159
  // The routed backend reports its own namespaced model verbatim: it is the
155
160
  // dispatched value, and collapsing it through the legacy resolver would
156
161
  // display a describer the runtime is not using (roadmap 190).
@@ -178,6 +183,17 @@ interface ClientIntegrationSyncOutcome {
178
183
  readonly profileId?: number;
179
184
  }
180
185
 
186
+ /** Only the Desktop write's own applied markers may change during a background projection. */
187
+ function clientProjectionConfig(config: OcxConfig): OcxConfig {
188
+ const snapshot = structuredClone(config);
189
+ const profile = snapshot.claudeCode?.desktopProfile;
190
+ if (profile) {
191
+ delete profile.appliedFingerprint;
192
+ delete profile.appliedAt;
193
+ }
194
+ return snapshot;
195
+ }
196
+
181
197
  /**
182
198
  * Re-inject native clients that are switched ON and every file integration whose
183
199
  * OpenCodex ownership record is the operator's durable opt-in.
@@ -201,17 +217,41 @@ export async function syncEnabledClientIntegrations(
201
217
  config: OcxConfig,
202
218
  deps: Pick<ManagementContext["deps"],
203
219
  "fetchAllModels" | "refreshOwnedCatalogIntegrations" | "writeDesktop3pConfig"> = {},
220
+ options: { unattended?: { isCurrent: () => boolean } } = {},
204
221
  ): Promise<ClientIntegrationSyncOutcome[]> {
205
222
  // A sibling instance passes its OWN port here; the Grok fence and the Desktop gateway profile
206
223
  // stay on the live owner's (`src/codex/sibling-start.ts`).
207
224
  if (port === undefined || siblingOfLivePort() !== null) return [];
208
- const { claudeDesktopIntegrationEnabled, grokIntegrationEnabled } = await import("../../codex/desired-state");
225
+ const projectionConfig = options.unattended ? clientProjectionConfig(config) : undefined;
226
+ const { claudeDesktopIntegrationEnabled, grokIntegrationEnabled, localClientSyncAllowed } = await import("../../codex/desired-state");
209
227
  const out: ClientIntegrationSyncOutcome[] = [];
228
+ const unattended = options.unattended;
229
+ const admit = () => {
230
+ if (!unattended!.isCurrent()) return false;
231
+ const fresh = loadConfig();
232
+ return localClientSyncAllowed(fresh) && isDeepStrictEqual(clientProjectionConfig(fresh), projectionConfig);
233
+ };
234
+ const stale = () => unattended !== undefined && !admit();
235
+ if (stale()) return out;
236
+ const grok = unattended ? await import("../../grok/inject") : undefined;
210
237
 
211
- if (grokIntegrationEnabled(config)) {
238
+ if (grokIntegrationEnabled(config) && (!grok || grok.grokManagedBlockPresent())) {
212
239
  try {
213
240
  const { syncGrokConfig } = await import("../../grok/sync");
214
- const r = await syncGrokConfig(port, config, config.hostname ? { hostname: config.hostname } : {});
241
+ if (stale()) return out;
242
+ const grokDeps = grok ? {
243
+ fetchAllModels: deps.fetchAllModels ?? (await import("../management-api")).fetchAllModels,
244
+ injectGrokConfig: ((...args: Parameters<typeof grok.injectGrokConfig>) =>
245
+ admit() && grokIntegrationEnabled(loadConfig()) && grok.grokManagedBlockPresent()
246
+ ? grok.injectGrokConfig(...args)
247
+ : { ok: true, changed: false, message: "Grok refresh skipped" }),
248
+ } : undefined;
249
+ const r = unattended
250
+ ? await syncGrokConfig(port, config, {
251
+ ...config.hostname ? { hostname: config.hostname } : {},
252
+ refreshOnly: { admit },
253
+ }, grokDeps)
254
+ : await syncGrokConfig(port, config, config.hostname ? { hostname: config.hostname } : {}, grokDeps);
215
255
  out.push(r.ok
216
256
  ? { client: "grok", ok: true, changed: r.changed === true }
217
257
  : { client: "grok", ok: false, reason: r.message });
@@ -220,9 +260,11 @@ export async function syncEnabledClientIntegrations(
220
260
  }
221
261
  }
222
262
 
263
+ if (stale()) return out;
223
264
  const { observeClaudeDesktopMode, resolveClaudeDesktopMode } = await import("../../claude/desktop-first-party");
224
265
  // A first-party Desktop must never get a gateway profile written and selected by a sync.
225
- if (claudeDesktopIntegrationEnabled(config) && resolveClaudeDesktopMode(config, observeClaudeDesktopMode(config)) !== "first-party") {
266
+ if (claudeDesktopIntegrationEnabled(config) && resolveClaudeDesktopMode(config, observeClaudeDesktopMode(config)) !== "first-party"
267
+ && (!unattended || !!config.claudeCode?.desktopProfile?.appliedFingerprint)) {
226
268
  try {
227
269
  const { writeDesktop3pConfig } = await import("../../claude/desktop-3p");
228
270
  const { desktopVisibleNativeSlugs, filterCatalogVisibleModels } = await import("../../codex/catalog");
@@ -236,12 +278,15 @@ export async function syncEnabledClientIntegrations(
236
278
  const latest = loadConfig();
237
279
  // Discovery awaited: the mode may have changed meanwhile. Re-resolve on the fresh read,
238
280
  // immediately before the writer, so a first-party switch during fetchAllModels still wins.
239
- if (claudeDesktopIntegrationEnabled(latest) && resolveClaudeDesktopMode(latest, observeClaudeDesktopMode(latest)) !== "first-party") {
281
+ if (stale()) return;
282
+ if (claudeDesktopIntegrationEnabled(latest) && resolveClaudeDesktopMode(latest, observeClaudeDesktopMode(latest)) !== "first-party"
283
+ && (!unattended || !!latest.claudeCode?.desktopProfile?.appliedFingerprint)) {
240
284
  const routed = filterCatalogVisibleModels(models, latest)
241
285
  .map(model => ({ provider: model.provider, id: model.id, contextWindow: model.contextWindow }));
242
286
  const writtenProfile = latest.claudeCode?.desktopProfile;
243
287
  const markerBaseline = captureDesktopAppliedMarker(writtenProfile);
244
- const r = (deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(
288
+ // Keep the attended writer's argument count unchanged, including its lock test seam.
289
+ const writeArgs: Parameters<typeof writeDesktop3pConfig> = [
245
290
  port,
246
291
  [...desktopVisibleNativeSlugs(latest)],
247
292
  routed,
@@ -249,7 +294,13 @@ export async function syncEnabledClientIntegrations(
249
294
  "static",
250
295
  writtenProfile,
251
296
  nativeContextLimits(latest),
252
- );
297
+ ];
298
+ if (unattended) {
299
+ writeArgs[7] = undefined;
300
+ writeArgs[8] = { appliedFingerprint: writtenProfile!.appliedFingerprint!, admit };
301
+ }
302
+ const r = (deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(...writeArgs);
303
+ if (unattended && r.reason === "desktop_refresh_only_skipped") return;
253
304
  if (!r.written || !r.fingerprint) {
254
305
  out.push({ client: "claude-desktop", ok: false, reason: r.reason ?? "Claude Desktop write failed" });
255
306
  } else {
@@ -267,6 +318,7 @@ export async function syncEnabledClientIntegrations(
267
318
  }
268
319
  }
269
320
 
321
+ if (stale()) return out;
270
322
  const { refreshOwnedCatalogIntegrations } = await import("../../integrations/catalog-refresh");
271
323
  const refreshOwned = deps.refreshOwnedCatalogIntegrations ?? refreshOwnedCatalogIntegrations;
272
324
  out.push(...await refreshOwned({
@@ -276,7 +328,10 @@ export async function syncEnabledClientIntegrations(
276
328
  },
277
329
  config,
278
330
  port,
279
- }, ["mcode", "pi", "aside", "raycast", "omo", "cline", "droid", "opencode", "kilo"]));
331
+ }, unattended
332
+ ? ["mcode", "pi", "aside", "raycast", "omo", "commandcode", "droid", "opencode", "kilo"]
333
+ : ["mcode", "pi", "aside", "raycast", "omo", "cline", "commandcode", "droid", "opencode", "kilo"],
334
+ ...unattended ? [{ refreshOnly: true, admit }] as const : []));
280
335
 
281
336
  return out;
282
337
  }
@@ -290,6 +345,8 @@ function publicVisionSidecarSettings(
290
345
  enabled: vs.enabled !== false,
291
346
  model: vision.model,
292
347
  backend: vs.backend,
348
+ ...(vs.anthropicInstance ? { anthropicInstance: vs.anthropicInstance } : {}),
349
+ anthropicPool: sidecarAnthropicPoolOptions(config, { ...vs, backend: vs.backend ?? (vision.models.find(row => row.value === vision.model)?.backend) }),
293
350
  reasoning: vision.reasoning,
294
351
  maxDescriptionsPerTurn: resolveMaxDescriptionsPerTurn(vs.maxDescriptionsPerTurn),
295
352
  timeoutMs: resolveVisionTimeoutMs(vs.timeoutMs),
@@ -882,6 +939,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
882
939
  enabled: ws.enabled !== false,
883
940
  model: ws.model ?? "gpt-5.6-luna",
884
941
  backend: ws.backend,
942
+ ...(ws.anthropicInstance ? { anthropicInstance: ws.anthropicInstance } : {}),
943
+ anthropicPool: sidecarAnthropicPoolOptions(config, ws),
885
944
  streamRoutedModelOutput: ws.streamRoutedModelOutput === true,
886
945
  ...(ws.xSearch ? { xSearch: ws.xSearch } : {}),
887
946
  },
@@ -902,10 +961,11 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
902
961
  if (raw.webSearch !== undefined && !isPlainRecord(raw.webSearch)) return jsonResponse({ error: "webSearch must be an object" }, 400);
903
962
  if (raw.vision !== undefined && !isPlainRecord(raw.vision)) return jsonResponse({ error: "vision must be an object" }, 400);
904
963
  const body = raw as {
905
- webSearch?: { enabled?: unknown; model?: unknown; backend?: unknown; reasoning?: unknown; streamRoutedModelOutput?: unknown; exaApiKey?: unknown; xSearch?: unknown };
964
+ webSearch?: { enabled?: unknown; model?: unknown; backend?: unknown; anthropicInstance?: unknown; reasoning?: unknown; streamRoutedModelOutput?: unknown; exaApiKey?: unknown; xSearch?: unknown };
906
965
  vision?: {
907
966
  model?: unknown;
908
967
  backend?: unknown;
968
+ anthropicInstance?: unknown;
909
969
  reasoning?: unknown;
910
970
  maxDescriptionsPerTurn?: unknown;
911
971
  enabled?: unknown;
@@ -913,6 +973,11 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
913
973
  };
914
974
  };
915
975
  const WEB_SEARCH_BACKENDS_UNION = ["openai", "anthropic", "xai", "gemini", "exa"] as const;
976
+ const instanceError = anthropicSidecarPatchError(config, {
977
+ ...(body.webSearch ? { webSearchSidecar: body.webSearch } : {}),
978
+ ...(body.vision ? { visionSidecar: body.vision } : {}),
979
+ });
980
+ if (instanceError) return jsonResponse({ error: instanceError }, 400);
916
981
  if (body.webSearch && body.webSearch.backend !== undefined && body.webSearch.backend !== null
917
982
  && !WEB_SEARCH_BACKENDS_UNION.includes(body.webSearch.backend as never)) {
918
983
  return jsonResponse({ error: "webSearch.backend must be openai, anthropic, xai, gemini, exa, or null" }, 400);
@@ -956,7 +1021,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
956
1021
  // rejection body, so a 400 cannot cost two provider fetches.
957
1022
  if (body.vision && typeof body.vision.model === "string" && body.vision.model !== "") {
958
1023
  const requested = body.vision.model;
959
- const candidates = await visionCandidateRows(config);
1024
+ const candidates = await visionCandidateRows({ ...config, visionSidecar: sidecarSettingsAfterPatch(config.visionSidecar, body.vision) });
960
1025
  const hint = body.vision.backend === "anthropic" || body.vision.backend === "openai"
961
1026
  || body.vision.backend === "routed"
962
1027
  ? body.vision.backend
@@ -994,7 +1059,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
994
1059
  // only case that owes a config.toml rewrite.
995
1060
  const webSearchEnabledBefore = config.webSearchSidecar?.enabled !== false;
996
1061
  if (body.webSearch) {
997
- const pairTouched = body.webSearch.model !== undefined || body.webSearch.backend !== undefined;
1062
+ const pairTouched = body.webSearch.model !== undefined || body.webSearch.backend !== undefined || body.webSearch.anthropicInstance !== undefined;
998
1063
  // Validate against the backend the caller SUBMITTED, across the whole
999
1064
  // union — not just openai/anthropic (#2457). The union check above has
1000
1065
  // already refused unknown literals, so a surviving string is a member;
@@ -1013,12 +1078,14 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
1013
1078
  ? body.webSearch.model || undefined
1014
1079
  : config.webSearchSidecar?.model;
1015
1080
  if (pairTouched && effectiveModel) {
1016
- const candidates = await webSearchCandidateRows(config);
1081
+ const candidates = await webSearchCandidateRows({ ...config, webSearchSidecar: sidecarSettingsAfterPatch(config.webSearchSidecar, body.webSearch) });
1017
1082
  if (webSearchModelIsRejected(effectiveBackend, effectiveModel, candidates)) {
1018
1083
  return jsonResponse(webSearchModelRejection("webSearch.model", effectiveBackend, effectiveModel, candidates), 400);
1019
1084
  }
1020
1085
  }
1021
1086
  const webSearchCandidate = { ...config.webSearchSidecar };
1087
+ if (body.webSearch.anthropicInstance === null) delete webSearchCandidate.anthropicInstance;
1088
+ else if (isAnthropicInstanceId(body.webSearch.anthropicInstance)) webSearchCandidate.anthropicInstance = body.webSearch.anthropicInstance;
1022
1089
  if (typeof body.webSearch.model === "string") {
1023
1090
  if (body.webSearch.model === "") delete webSearchCandidate.model;
1024
1091
  else webSearchCandidate.model = body.webSearch.model;
@@ -1098,6 +1165,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
1098
1165
  }
1099
1166
  if (body.vision) {
1100
1167
  config.visionSidecar = { ...config.visionSidecar };
1168
+ if (body.vision.anthropicInstance === null) delete config.visionSidecar.anthropicInstance;
1169
+ else if (isAnthropicInstanceId(body.vision.anthropicInstance)) config.visionSidecar.anthropicInstance = body.vision.anthropicInstance;
1101
1170
  if (typeof body.vision.model === "string") {
1102
1171
  if (body.vision.model === "") delete config.visionSidecar.model;
1103
1172
  else config.visionSidecar.model = body.vision.model;
@@ -1143,6 +1212,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
1143
1212
  enabled: ws.enabled !== false,
1144
1213
  model: ws.model ?? "gpt-5.6-luna",
1145
1214
  backend: ws.backend,
1215
+ ...(ws.anthropicInstance ? { anthropicInstance: ws.anthropicInstance } : {}),
1216
+ anthropicPool: sidecarAnthropicPoolOptions(config, ws),
1146
1217
  streamRoutedModelOutput: ws.streamRoutedModelOutput === true,
1147
1218
  ...(ws.xSearch ? { xSearch: ws.xSearch } : {}),
1148
1219
  },
@@ -64,8 +64,8 @@ export interface ManagementApiDeps {
64
64
  refreshOwnedCatalogIntegrations?: typeof refreshOwnedCatalogIntegrations;
65
65
  /** Platform seam for capability projections; does not alter host-level startup behavior. */
66
66
  platform?: NodeJS.Platform;
67
- toggleCodexMultiAgentV2?: (enabled: boolean) => void;
68
- toggleDefaultModeRequestUserInput?: (enabled: boolean) => void;
67
+ toggleCodexMultiAgentV2?: (enabled: boolean, env: NodeJS.ProcessEnv, validateBeforeSpawn: () => void) => void;
68
+ toggleDefaultModeRequestUserInput?: (enabled: boolean, env: NodeJS.ProcessEnv, validateBeforeSpawn: () => void) => void;
69
69
  createManagementConvergeCodex?: (config: Readonly<OcxConfig>) => ConvergeCodex;
70
70
  /** Test-only destination for best-effort Claude agent-definition sync. */
71
71
  claudeAgentConfigDir?: string;
@@ -1,4 +1,5 @@
1
1
  import { jsonResponse } from "../auth-cors";
2
+ import { jevDecisionEndpointUrl } from "../../combos/jev-decision-contract";
2
3
  import type { ManagementContext } from "./context";
3
4
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
4
5
  import { isPlainRecord } from "./shared";
@@ -29,7 +30,7 @@ function decisionServiceIssue(
29
30
  provider: DecisionRow,
30
31
  isSystemOneEndpoint: (url: string) => boolean,
31
32
  ): { url: string; model: string; issue?: "disabled" | "endpoint" | "model" } {
32
- const url = typeof provider.baseUrl === "string" ? provider.baseUrl.trim().replace(/\/+$/, "") : "";
33
+ const url = typeof provider.baseUrl === "string" ? jevDecisionEndpointUrl(provider.baseUrl) : "";
33
34
  const model = provider.defaultModel?.trim() || provider.models?.[0]?.trim() || "";
34
35
  const issue = provider.disabled === true
35
36
  ? "disabled"