@bitkyc08/opencodex 2.79.0 → 2.81.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 (245) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/App-7yhoys8a.js +54 -0
  3. package/gui/dist/assets/Tray-EwAOsP0S.js +1 -0
  4. package/gui/dist/assets/index-RsoaAxXY.js +94 -0
  5. package/gui/dist/assets/quota-summary-D2lN8tDm.js +1 -0
  6. package/gui/dist/index.html +1 -1
  7. package/gui/dist/provider-icons/claude-green.svg +1 -0
  8. package/package.json +3 -2
  9. package/src/adapters/anthropic-model-contract.ts +8 -2
  10. package/src/adapters/anthropic.ts +7 -2
  11. package/src/adapters/codebuddy/mcp-server.ts +2 -25
  12. package/src/adapters/codebuddy/tool-bridge.ts +1 -7
  13. package/src/adapters/coding-agent/mcp-server.ts +33 -0
  14. package/src/adapters/coding-agent/protocol.ts +122 -17
  15. package/src/adapters/coding-agent/tool-catalog.ts +75 -0
  16. package/src/adapters/coding-agent/turn.ts +101 -44
  17. package/src/adapters/cursor/catalog.ts +8 -0
  18. package/src/adapters/cursor/effort-map.ts +2 -0
  19. package/src/adapters/cursor/live-transport.ts +5 -3
  20. package/src/adapters/cursor/native-exec-fs.ts +12 -9
  21. package/src/adapters/cursor/native-exec.ts +13 -5
  22. package/src/adapters/cursor/protobuf-request.ts +13 -7
  23. package/src/adapters/cursor/request-builder.ts +23 -10
  24. package/src/adapters/cursor/tool-definitions.ts +9 -6
  25. package/src/adapters/cursor/tool-guidance.ts +29 -8
  26. package/src/adapters/cursor/tool-naming.ts +40 -7
  27. package/src/adapters/cursor/tool-wording.ts +46 -0
  28. package/src/adapters/cursor.ts +3 -0
  29. package/src/adapters/devin/live-models.ts +2 -0
  30. package/src/adapters/devin.ts +25 -11
  31. package/src/adapters/kiro-retry.ts +9 -4
  32. package/src/adapters/openai-chat/messages.ts +7 -4
  33. package/src/adapters/physical-send.ts +8 -5
  34. package/src/adapters/qoder/adapter.ts +41 -6
  35. package/src/adapters/qoder/scaffold-guard.ts +2 -3
  36. package/src/adapters/zed.ts +16 -1
  37. package/src/claude/inbound-cache-stabilize.ts +4 -9
  38. package/src/claude/inbound.ts +10 -0
  39. package/src/claude/intercept/model-bindings.ts +1 -0
  40. package/src/claude/intercept/picker-listener.ts +71 -4
  41. package/src/cli/account-anthropic-threshold.ts +14 -5
  42. package/src/cli/account-api.ts +3 -1
  43. package/src/cli/account-extended.ts +13 -9
  44. package/src/cli/account-policy-dto.ts +1 -1
  45. package/src/cli/account-policy.ts +13 -4
  46. package/src/cli/account.ts +3 -3
  47. package/src/cli/capabilities-accounts.ts +2 -2
  48. package/src/cli/capabilities-agents-routing.ts +24 -0
  49. package/src/cli/capabilities-integrations.ts +16 -0
  50. package/src/cli/codex-shim-autorestore.ts +3 -0
  51. package/src/cli/dispatch.ts +10 -1
  52. package/src/cli/help.ts +4 -1
  53. package/src/cli/index.ts +11 -5
  54. package/src/cli/integration-plan-dto.ts +1 -0
  55. package/src/cli/integration-preview.ts +8 -1
  56. package/src/cli/integrations.ts +32 -3
  57. package/src/cli/message-args.ts +34 -0
  58. package/src/cli/message-command.ts +43 -0
  59. package/src/cli/message-runtime.ts +6 -0
  60. package/src/cli/observe.ts +3 -1
  61. package/src/cli/provider.ts +29 -6
  62. package/src/cli/registry.ts +24 -2
  63. package/src/clients/config-export/commandcode.ts +131 -0
  64. package/src/clients/config-export/contracts.ts +12 -0
  65. package/src/clients/config-export.ts +64 -1
  66. package/src/codex/auth-api/account-list.ts +11 -3
  67. package/src/codex/auth-api/main-account-probe.ts +25 -8
  68. package/src/codex/auth-api/pool-mode-gate.ts +33 -9
  69. package/src/codex/auth-api/pool-quota-probe.ts +15 -1
  70. package/src/codex/auth-api/routes.ts +1 -0
  71. package/src/codex/autostart-health.ts +3 -3
  72. package/src/codex/catalog/auto-review.ts +13 -6
  73. package/src/codex/catalog/build-entries.ts +15 -7
  74. package/src/codex/catalog/control-plane.ts +45 -0
  75. package/src/codex/catalog/effort.ts +11 -1
  76. package/src/codex/catalog/gather-capture.ts +5 -3
  77. package/src/codex/catalog/parsing.ts +2 -1
  78. package/src/codex/catalog/provider-models.ts +15 -9
  79. package/src/codex/control-plane-models.ts +6 -0
  80. package/src/codex/journal.ts +36 -5
  81. package/src/codex/shim-overlay.ts +9 -1
  82. package/src/combos/jev-decision-contract.ts +25 -2
  83. package/src/combos/jev.ts +6 -3
  84. package/src/combos/types.ts +1 -1
  85. package/src/config/diagnostics.ts +27 -16
  86. package/src/config/load-degrade.ts +34 -0
  87. package/src/config/schema/anthropic-account-pool.ts +143 -0
  88. package/src/config/schema/config-schema.ts +7 -1
  89. package/src/config/schema/leaf-validators.ts +4 -0
  90. package/src/config.ts +2 -0
  91. package/src/generated/compatibility-version.json +347 -199
  92. package/src/generated/model-metadata.ts +5 -4
  93. package/src/images/loop.ts +2 -1
  94. package/src/integrations/catalog-refresh.ts +1 -1
  95. package/src/integrations/mutation-plan.ts +7 -0
  96. package/src/integrations/registry.ts +8 -0
  97. package/src/integrations/state.ts +9 -2
  98. package/src/integrations/writer.ts +8 -2
  99. package/src/lib/lab-live-route-production.ts +9 -1
  100. package/src/lib/request-execution-budget.ts +236 -58
  101. package/src/lib/spend-pool-alias-validation.ts +79 -0
  102. package/src/lib/spend-pool-continuity.ts +34 -0
  103. package/src/lib/spend-reservation-ledger.ts +412 -50
  104. package/src/lib/state-store-registrations.ts +20 -3
  105. package/src/lib/upstream-retry.ts +17 -5
  106. package/src/lib/workflow-budget.ts +53 -11
  107. package/src/messaging/budget.ts +45 -0
  108. package/src/messaging/discovery.ts +57 -0
  109. package/src/messaging/envelope.ts +49 -0
  110. package/src/messaging/input.ts +37 -0
  111. package/src/messaging/rpc.ts +196 -0
  112. package/src/messaging/send.ts +58 -0
  113. package/src/messaging/socket.ts +72 -0
  114. package/src/messaging/types.ts +29 -0
  115. package/src/oauth/anthropic-account-refusal.ts +51 -17
  116. package/src/oauth/anthropic-account-threshold.ts +8 -2
  117. package/src/oauth/anthropic-continuity.ts +15 -1
  118. package/src/oauth/anthropic-model-quota.ts +120 -48
  119. package/src/oauth/anthropic-model-routes.ts +11 -3
  120. package/src/oauth/anthropic-oauth-definitions.ts +98 -0
  121. package/src/oauth/anthropic-pool-config.ts +30 -0
  122. package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
  123. package/src/oauth/anthropic-routing.ts +1202 -866
  124. package/src/oauth/anthropic-send-ownership.ts +41 -0
  125. package/src/oauth/anthropic.ts +4 -1
  126. package/src/oauth/generic-account-failover.ts +3 -1
  127. package/src/oauth/health.ts +8 -6
  128. package/src/oauth/index.ts +90 -173
  129. package/src/oauth/kiro.ts +22 -9
  130. package/src/oauth/login-cli.ts +2 -0
  131. package/src/oauth/model-discovery-auth.ts +43 -0
  132. package/src/oauth/pool-kernel.ts +11 -5
  133. package/src/oauth/pool-settings-capability.ts +5 -3
  134. package/src/oauth/store-anthropic-instance.ts +87 -0
  135. package/src/oauth/store.ts +67 -9
  136. package/src/oauth/token-guardian.ts +2 -0
  137. package/src/oauth/types.ts +2 -0
  138. package/src/protocols/plan-snapshot.ts +5 -0
  139. package/src/protocols/settings.ts +20 -5
  140. package/src/providers/anthropic-instance-id.ts +47 -0
  141. package/src/providers/anthropic-instance.ts +48 -0
  142. package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
  143. package/src/providers/derive.ts +2 -0
  144. package/src/providers/kiro-models.ts +3 -0
  145. package/src/providers/label.ts +1 -1
  146. package/src/providers/model-rename-fields.ts +2 -0
  147. package/src/providers/quota/account-cache.ts +37 -11
  148. package/src/providers/quota/anthropic-account-quota.ts +93 -0
  149. package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
  150. package/src/providers/quota/report-cache.ts +24 -1
  151. package/src/providers/quota/vendor-probes-oauth.ts +46 -14
  152. package/src/providers/quota.ts +22 -32
  153. package/src/providers/registry/entries-core.ts +35 -30
  154. package/src/providers/registry/model-ids.ts +1 -0
  155. package/src/providers/registry/model-seeds.ts +7 -4
  156. package/src/providers/registry/types.ts +3 -0
  157. package/src/providers/registry.ts +3 -3
  158. package/src/responses/spill-store.ts +5 -1
  159. package/src/responses/state/metrics.ts +87 -0
  160. package/src/responses/state/snapshot-codec.ts +6 -0
  161. package/src/responses/state/spill-queue.ts +1 -0
  162. package/src/responses/state/unforced-store-false.ts +55 -0
  163. package/src/responses/state.ts +50 -66
  164. package/src/router.ts +28 -4
  165. package/src/routing/compatibility/assemble.ts +1 -1
  166. package/src/routing/quota.ts +7 -3
  167. package/src/server/auth-cors.ts +5 -0
  168. package/src/server/chat-completions.ts +6 -1
  169. package/src/server/chat-native.ts +45 -25
  170. package/src/server/claude-messages.ts +16 -2
  171. package/src/server/gui-static.ts +38 -4
  172. package/src/server/index/spend-ledger-lifecycle.ts +10 -4
  173. package/src/server/index.ts +16 -16
  174. package/src/server/inference/context.ts +1 -1
  175. package/src/server/lifecycle.ts +1 -1
  176. package/src/server/management/agent-settings-routes.ts +30 -6
  177. package/src/server/management/anthropic-account-threshold.ts +12 -9
  178. package/src/server/management/anthropic-pool-settings.ts +72 -0
  179. package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
  180. package/src/server/management/config-routes.ts +27 -7
  181. package/src/server/management/decision-routes.ts +2 -1
  182. package/src/server/management/integration-routes.ts +25 -2
  183. package/src/server/management/oauth-account-routes.ts +79 -98
  184. package/src/server/management/provider-instance-ownership.ts +44 -0
  185. package/src/server/management/provider-routes.ts +21 -28
  186. package/src/server/management/route-registry.ts +3 -1
  187. package/src/server/management/routing-profile-routes.ts +4 -4
  188. package/src/server/management/system-restart.ts +44 -14
  189. package/src/server/management/system-routes.ts +24 -2
  190. package/src/server/management/vision-sidecar-options.ts +9 -6
  191. package/src/server/management/web-search-sidecar-options.ts +51 -2
  192. package/src/server/management-auth.ts +6 -0
  193. package/src/server/messages-native-eligibility.ts +3 -2
  194. package/src/server/messages-native-oauth.ts +86 -41
  195. package/src/server/messages-native-scope.ts +4 -1
  196. package/src/server/messages-native-selector.ts +39 -0
  197. package/src/server/messages-native.ts +84 -25
  198. package/src/server/request-log.ts +5 -0
  199. package/src/server/responses/adapter-continuation.ts +29 -17
  200. package/src/server/responses/adapter-dispatch.ts +47 -17
  201. package/src/server/responses/antigravity-validation-refusal.ts +20 -6
  202. package/src/server/responses/combo-requested-effort.ts +14 -0
  203. package/src/server/responses/compact.ts +25 -8
  204. package/src/server/responses/compaction-recovery.ts +1 -1
  205. package/src/server/responses/core-combo-native.ts +4 -1
  206. package/src/server/responses/core-combo.ts +21 -18
  207. package/src/server/responses/core-normalize.ts +1 -0
  208. package/src/server/responses/core-options.ts +3 -1
  209. package/src/server/responses/core-replay.ts +5 -0
  210. package/src/server/responses/passthrough-delivery.ts +7 -3
  211. package/src/server/responses/passthrough-dispatch.ts +21 -8
  212. package/src/server/responses/request-prepare.ts +37 -5
  213. package/src/server/responses/request-send-budget.ts +74 -37
  214. package/src/server/responses/request-spend.ts +325 -33
  215. package/src/server/responses/request-transport.ts +132 -52
  216. package/src/server/responses/run-turn-execution.ts +11 -3
  217. package/src/server/responses/sidecar-execution.ts +96 -14
  218. package/src/server/responses-undeclared-tool-guard.ts +5 -1
  219. package/src/server/workflow-refusal.ts +30 -0
  220. package/src/service/desktop-startup.ts +100 -3
  221. package/src/service/ownership-mutation-lease.d.mts +1 -1
  222. package/src/service/ownership-mutation-lease.mjs +5 -7
  223. package/src/sidecar/anthropic-binding.ts +79 -0
  224. package/src/sidecar/auth.ts +76 -2
  225. package/src/types/anthropic-account-pool.ts +33 -0
  226. package/src/types/config.ts +12 -28
  227. package/src/types/provider.ts +5 -0
  228. package/src/types/tools.ts +7 -0
  229. package/src/types.ts +4 -0
  230. package/src/usage/cost.ts +3 -2
  231. package/src/usage/expected-prices.ts +66 -14
  232. package/src/vision/anthropic-describe.ts +16 -3
  233. package/src/vision/index.ts +3 -0
  234. package/src/vision/plan.ts +19 -6
  235. package/src/web-search/alpha-search.ts +7 -1
  236. package/src/web-search/anthropic-executor.ts +16 -4
  237. package/src/web-search/header-deadline.ts +67 -0
  238. package/src/web-search/index.ts +7 -3
  239. package/src/web-search/loop.ts +32 -10
  240. package/src/web-search/passthrough-bridge.ts +4 -1
  241. package/src/web-search/sidecar-providers.ts +4 -3
  242. package/gui/dist/assets/App-BOUa-ek3.js +0 -54
  243. package/gui/dist/assets/Tray-DUtzB4zF.js +0 -1
  244. package/gui/dist/assets/index-DwHNDF4s.js +0 -94
  245. package/gui/dist/assets/quota-summary-BuL4WIBo.js +0 -1
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Dashboard memory-card drain-and-restart (#563).
3
3
  *
4
- * Longer than POST /api/stop's short drain: waits up to 60s for active turns,
5
- * then respawns. Never runs restoreNativeCodex / stripGrokConfig — this is a
4
+ * Waits up to 60s for active turns unless a manual API caller explicitly opts
5
+ * into a shorter grace, with the existing 60s cleanup watchdog, then respawns. Never runs restoreNativeCodex / stripGrokConfig — this is a
6
6
  * recycle to reclaim RSS, not a teardown.
7
7
  *
8
8
  * Respawn policy (matches real supervisor configs in src/service.ts):
@@ -37,6 +37,7 @@
37
37
  * Checked before the service rule: that app, not a service manager, is the parent.
38
38
  */
39
39
  import {
40
+ abortAndReleaseAllTurns,
40
41
  acquireTemporaryDrain,
41
42
  beginShutdownDrain,
42
43
  drainAndShutdown,
@@ -64,8 +65,11 @@ import { spawnReplacementStart } from "../restart-replacement";
64
65
  export { MEMORY_DRAIN_RESTART_MS, REPLACEMENT_READY_TIMEOUT_MS } from "../../lib/system-restart-contract";
65
66
  export { waitForReplacementReady, type ReplacementReadinessIo } from "../restart-replacement";
66
67
  export const DEADLINE_LISTENER_STOP_TIMEOUT_MS = 5_000;
68
+ const RESTART_RESPONSE_FLUSH_DELAY_MS = 200;
67
69
 
68
70
  export interface SystemRestartIo {
71
+ /** Cancel and release admitted turns without stopping the listener or process. */
72
+ abortActiveTurns?: () => void;
69
73
  acquireTemporaryDrain?: () => { release(): void } | null;
70
74
  drainAndShutdown?: typeof drainAndShutdown;
71
75
  /** True when a background service can actually respawn this process after exit(1). */
@@ -92,6 +96,11 @@ export interface SystemRestartIo {
92
96
  now?: () => number;
93
97
  }
94
98
 
99
+ export interface SystemRestartOptions {
100
+ /** Explicit API opt-in; omission preserves the existing 60s drain. */
101
+ drainGraceMs?: number;
102
+ }
103
+
95
104
  export interface SystemRestartAdmission {
96
105
  /** Only the caller that created this pending restart receives its veto. */
97
106
  onAccepted?: (veto: () => void) => void;
@@ -100,8 +109,9 @@ export interface SystemRestartAdmission {
100
109
  }
101
110
 
102
111
  let restartIo: SystemRestartIo = {};
103
- /** Prevents double-scheduling in the 200ms window before drainAndShutdown sets draining. */
112
+ /** Prevents double-scheduling during the 200ms response-flush window. */
104
113
  let restartAccepted = false;
114
+ let acceptedDrainGraceMs = MEMORY_DRAIN_RESTART_MS;
105
115
 
106
116
  type RestartDrainOutcome = "completed" | "failed" | "rejected" | "deadline";
107
117
  type BoundedSettlementOutcome = "completed" | "rejected" | "deadline";
@@ -173,6 +183,7 @@ export function noteExplicitShutdownRequested(): void {
173
183
  export function setSystemRestartIoForTests(io: SystemRestartIo = {}): void {
174
184
  restartIo = io;
175
185
  restartAccepted = false;
186
+ acceptedDrainGraceMs = MEMORY_DRAIN_RESTART_MS;
176
187
  explicitShutdownRequested = false;
177
188
  }
178
189
 
@@ -324,10 +335,10 @@ async function completeDeadlineRestartHandoff(
324
335
 
325
336
  /**
326
337
  * Accept a drain-and-restart request. Returns immediately; the drain +
327
- * respawn runs on a short timer so the HTTP response can flush first.
338
+ * respawn waits 200ms for the HTTP response to flush; shorter grace cuts turns separately.
328
339
  * Idempotent while already draining: returns the accepted shape again.
329
340
  */
330
- export function acceptSystemRestart(io: SystemRestartIo = restartIo, admission: SystemRestartAdmission = {}): {
341
+ export function acceptSystemRestart(io: SystemRestartIo = restartIo, admission: SystemRestartAdmission = {}, options: SystemRestartOptions = {}): {
331
342
  accepted: true;
332
343
  alreadyDraining: boolean;
333
344
  activeTurnCount: number;
@@ -337,16 +348,22 @@ export function acceptSystemRestart(io: SystemRestartIo = restartIo, admission:
337
348
  ?? io.isDraining
338
349
  ?? isShutdownDraining;
339
350
  const alreadyDraining = restartAccepted || shutdownActive();
351
+ const drainGraceMs = alreadyDraining
352
+ ? (restartAccepted ? acceptedDrainGraceMs : MEMORY_DRAIN_RESTART_MS)
353
+ : (options.drainGraceMs ?? MEMORY_DRAIN_RESTART_MS);
340
354
  const activeTurnCount = (io.getActiveTurnCount ?? getActiveTurnCount)();
341
355
  const schedule = io.schedule ?? ((fn, ms) => { setTimeout(() => { void fn(); }, ms); });
342
356
 
343
357
  if (!alreadyDraining) {
344
358
  restartAccepted = true;
359
+ acceptedDrainGraceMs = drainGraceMs;
345
360
  const automatic = Boolean(admission.onAccepted);
346
361
  const temporaryDrain = automatic
347
362
  ? (io.acquireTemporaryDrain ?? (() => acquireTemporaryDrain("automatic-restart")))()
348
363
  : null;
364
+ let cancelEarlyCut: (() => void) | undefined;
349
365
  const releasePending = () => {
366
+ cancelEarlyCut?.();
350
367
  temporaryDrain?.release();
351
368
  restartAccepted = false;
352
369
  };
@@ -363,13 +380,27 @@ export function acceptSystemRestart(io: SystemRestartIo = restartIo, admission:
363
380
  releasePending();
364
381
  });
365
382
  const now = io.now ?? Date.now;
366
- const restartDeadlineMs = now() + MEMORY_DRAIN_RESTART_MS;
383
+ const acceptedAtMs = now();
384
+ const restartDeadlineMs = acceptedAtMs + MEMORY_DRAIN_RESTART_MS;
385
+ const drainDeadlineMs = acceptedAtMs + drainGraceMs;
386
+ const scheduleDeadline = io.scheduleDeadline ?? ((fn, ms) => {
387
+ const timer = setTimeout(fn, ms);
388
+ return () => clearTimeout(timer);
389
+ });
367
390
  // Reject new data-plane traffic immediately (503), before the 200ms response-flush delay.
368
391
  if (!automatic) {
369
392
  if (io.beginShutdownDrain) io.beginShutdownDrain();
370
393
  else if (io.setDraining) io.setDraining(true);
371
394
  else beginShutdownDrain();
372
395
  }
396
+ // A shorter grace cuts admitted turns at acceptance + grace, but listener stop
397
+ // and process exit must still wait for the full response-flush window.
398
+ if (!vetoed && drainGraceMs < RESTART_RESPONSE_FLUSH_DELAY_MS) {
399
+ cancelEarlyCut = scheduleDeadline(() => {
400
+ if (vetoed) return;
401
+ (io.abortActiveTurns ?? (() => abortAndReleaseAllTurns(new Error("server shutdown"))))();
402
+ }, drainGraceMs);
403
+ }
373
404
  schedule(async () => {
374
405
  if (vetoed) return;
375
406
  pending = false;
@@ -390,12 +421,11 @@ export function acceptSystemRestart(io: SystemRestartIo = restartIo, admission:
390
421
  // closes the listener and makes both the server ref and runtime metadata stale.
391
422
  const restartPort = (io.listenPort ?? resolveListenPort)();
392
423
  const drain = io.drainAndShutdown ?? drainAndShutdown;
393
- const remainingMs = Math.max(0, restartDeadlineMs - now());
394
- const drainPromise = Promise.resolve().then(() => drain(undefined, remainingMs));
395
- const scheduleDeadline = io.scheduleDeadline ?? ((fn, ms) => {
396
- const timer = setTimeout(fn, ms);
397
- return () => clearTimeout(timer);
398
- });
424
+ // Bound active/scoped-drain waiting independently of cleanup and readiness.
425
+ // A cut turn may already have executed upstream; never replay it automatically.
426
+ const drainPromise = Promise.resolve().then(
427
+ () => drain(undefined, Math.max(0, drainDeadlineMs - now())),
428
+ );
399
429
  const drainOutcome = await waitForRestartDrain(drainPromise, restartDeadlineMs, now, scheduleDeadline);
400
430
  if (!canHandoff()) return;
401
431
  const exitProcess = io.exitProcess ?? ((code: number) => { process.exit(code); });
@@ -451,13 +481,13 @@ export function acceptSystemRestart(io: SystemRestartIo = restartIo, admission:
451
481
  }
452
482
  (io.markRecycling ?? markRecyclingForExit)();
453
483
  exitProcess(drainOutcome === "failed" || drainOutcome === "rejected" ? 1 : 0);
454
- }, 200);
484
+ }, RESTART_RESPONSE_FLUSH_DELAY_MS);
455
485
  }
456
486
 
457
487
  return {
458
488
  accepted: true,
459
489
  alreadyDraining,
460
490
  activeTurnCount,
461
- drainTimeoutMs: MEMORY_DRAIN_RESTART_MS,
491
+ drainTimeoutMs: drainGraceMs,
462
492
  };
463
493
  }
@@ -30,6 +30,7 @@ import { inspectResponseSpillStorage, responseStateMetrics, type ResponseSpillDi
30
30
  import { appOwnedBytesSnapshot } from "../../lib/app-owned-memory";
31
31
  import { readWindowsReplaceRetryCounters } from "../../lib/windows-atomic-replace";
32
32
  import {
33
+ MEMORY_DRAIN_RESTART_MS,
33
34
  SYSTEM_RESTART_EXPECTED_PID_HEADER,
34
35
  parseExpectedSystemRestartPid,
35
36
  } from "../../lib/system-restart-contract";
@@ -46,6 +47,7 @@ import type {
46
47
  } from "../../codex/app-server-restart-service";
47
48
  import type { ManagementContext } from "./context";
48
49
  import { acceptSystemRestart } from "./system-restart";
50
+ import { readOptionalManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
49
51
 
50
52
  const ENDPOINT_SAMPLE_LIMIT = 60;
51
53
  // The spill report reads the snapshot and walks the spill directory synchronously,
@@ -179,8 +181,28 @@ export async function handleSystemRoutes(ctx: ManagementContext): Promise<Respon
179
181
  }, 409, req, config);
180
182
  }
181
183
 
182
- // Longer informed drain than /api/stop; does not tear down Codex/Grok injection.
183
- const result = acceptSystemRestart();
184
+ let body: unknown;
185
+ try { body = await readOptionalManagementJsonBody(req); }
186
+ catch (error) {
187
+ rethrowManagementBodyTooLarge(error);
188
+ return jsonResponse({ success: false, error: "Invalid restart JSON body." }, 400, req, config);
189
+ }
190
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
191
+ return jsonResponse({ success: false, error: "Invalid restart JSON body." }, 400, req, config);
192
+ }
193
+ const drainGraceMs = (body as Record<string, unknown>).drainGraceMs;
194
+ if (drainGraceMs !== undefined && (
195
+ typeof drainGraceMs !== "number" || !Number.isSafeInteger(drainGraceMs)
196
+ || drainGraceMs < 1 || drainGraceMs > MEMORY_DRAIN_RESTART_MS
197
+ )) {
198
+ return jsonResponse({ success: false, error: "drainGraceMs must be an integer from 1 to 60000." }, 400, req, config);
199
+ }
200
+ // The process capability signs the restart target, not this new body option.
201
+ if (drainGraceMs !== undefined && ctx.principal === "system-restart-capability") {
202
+ return jsonResponse({ success: false, error: "Short grace requires management authentication." }, 403, req, config);
203
+ }
204
+ // Omission retains the informed 60s drain; only an explicit API request shortens it.
205
+ const result = acceptSystemRestart(undefined, {}, { drainGraceMs });
184
206
  return jsonResponse({
185
207
  success: true,
186
208
  message: result.alreadyDraining
@@ -8,7 +8,7 @@
8
8
  * the same as no gate at all.
9
9
  */
10
10
  import type { OcxConfig } from "../../types";
11
- import { findAnthropicVisionProvider, type AnthropicVisionProvider } from "../../vision";
11
+ import { type AnthropicVisionProvider } from "../../vision";
12
12
  import { VISION_BACKENDS } from "../../vision/backends";
13
13
  import {
14
14
  modelAcceptsImageInput,
@@ -18,7 +18,7 @@ import {
18
18
  type VisionSidecarBackend,
19
19
  } from "../../vision/eligibility";
20
20
  import { pickerVisibleSidecarCandidates } from "../../sidecar/candidates";
21
- import { resolveSidecarAuth } from "../../sidecar/auth";
21
+ import { sidecarOptionsAuth } from "./web-search-sidecar-options";
22
22
 
23
23
  /**
24
24
  * Backends whose executor could actually run (#2188 roadmap 170): openai
@@ -38,7 +38,7 @@ export function enabledVisionBackends(
38
38
  config: OcxConfig,
39
39
  anthropicSidecar: AnthropicVisionProvider | undefined,
40
40
  ): VisionSidecarBackend[] {
41
- const auth = resolveSidecarAuth(config);
41
+ const auth = sidecarOptionsAuth(config, config.visionSidecar?.anthropicInstance);
42
42
  // Preserve the caller's resolution for the anthropic side: the descriptor
43
43
  // reads the shared auth module, but a caller that already resolved "no
44
44
  // executor" must not see anthropic options it cannot dispatch. The filter
@@ -52,7 +52,7 @@ export function enabledVisionBackends(
52
52
  // the UNIVERSAL sides: when neither resolves, both are offered so the picker
53
53
  // stays populated (permissive-unknown rule; test 6 pins it).
54
54
  if (!active.includes("openai") && !active.includes("anthropic")) {
55
- return ["openai", "anthropic", ...active];
55
+ return ["openai", ...(config.visionSidecar?.anthropicInstance ? [] : ["anthropic" as const]), ...active];
56
56
  }
57
57
  return active;
58
58
  }
@@ -67,7 +67,7 @@ export function enabledVisionBackends(
67
67
  * pre-filtering here would deaden the gate (review F1: reject → allow flip).
68
68
  */
69
69
  export async function visionCandidateRows(config: OcxConfig): Promise<VisionCandidateModel[]> {
70
- const auth = resolveSidecarAuth(config);
70
+ const auth = sidecarOptionsAuth(config, config.visionSidecar?.anthropicInstance);
71
71
  const all = await pickerVisibleSidecarCandidates(config, auth);
72
72
  return all.map(candidate => ({
73
73
  provider: candidate.provider,
@@ -158,10 +158,13 @@ export function visionDescriberRejection(
158
158
  config: OcxConfig,
159
159
  candidates: readonly VisionCandidateModel[],
160
160
  ): { error: string; allowed: string[] } {
161
+ const auth = sidecarOptionsAuth(config, config.visionSidecar?.anthropicInstance);
162
+ const anthropic = auth.anthropicProviderName && auth.anthropicProvider
163
+ ? { providerName: auth.anthropicProviderName, provider: auth.anthropicProvider, config } : undefined;
161
164
  return {
162
165
  error: `${field} "${requested}" cannot describe images: it has no image input support, or it is a model the vision sidecar describes FOR.`,
163
166
  // The rejection path is not hot, so it resolves the executor itself rather than
164
167
  // making every 400 caller thread one through.
165
- allowed: visionModelOptionsFrom(config, candidates, findAnthropicVisionProvider(config)).map(option => option.value),
168
+ allowed: visionModelOptionsFrom(config, candidates, anthropic).map(option => option.value),
166
169
  };
167
170
  }
@@ -14,7 +14,9 @@
14
14
  * id outside (candidates ∪ auth slots) can never run and is refused.
15
15
  */
16
16
  import type { OcxConfig } from "../../types";
17
- import { AUTH_SLOT_MODELS, resolveSidecarAuth } from "../../sidecar/auth";
17
+ import { AUTH_SLOT_MODELS, resolveSidecarAuth, resolveAnthropicSidecarAuth, resolveAnthropicHelperInstance, AnthropicHelperUnavailableError, type SidecarAuthState } from "../../sidecar/auth";
18
+ import { ANTHROPIC_INSTANCE_IDS, isAnthropicInstanceId, type AnthropicInstanceId } from "../../providers/anthropic-instance-id";
19
+ import { configuredAnthropicInstance } from "../../providers/anthropic-instance";
18
20
  import { pickerVisibleSidecarCandidates, type SidecarCandidate } from "../../sidecar/candidates";
19
21
  import { WEB_SEARCH_BACKENDS, webSearchSidecarCandidates } from "../../web-search/backends";
20
22
  import type { WebSearchBackendId } from "../../web-search/index";
@@ -24,6 +26,53 @@ import type { WebSearchBackendId } from "../../web-search/index";
24
26
  // simply never produce rows.
25
27
  export type WebSearchBackend = WebSearchBackendId;
26
28
 
29
+ export function sidecarAnthropicPoolOptions(config: OcxConfig,
30
+ settings: { backend?: string; anthropicInstance?: AnthropicInstanceId }, parentProviderName?: string): {
31
+ backend?: string; parent?: AnthropicInstanceId; selected?: AnthropicInstanceId;
32
+ resolved?: AnthropicInstanceId; mixed: boolean; available: AnthropicInstanceId[];
33
+ code?: "anthropic_helper_unavailable";
34
+ } {
35
+ const parent = configuredAnthropicInstance(config, parentProviderName);
36
+ const available = ANTHROPIC_INSTANCE_IDS.filter(instance => resolveAnthropicSidecarAuth(config, instance) !== undefined);
37
+ const base = { ...(settings.backend ? { backend: settings.backend } : {}), ...(parent ? { parent } : {}), ...(settings.anthropicInstance ? { selected: settings.anthropicInstance } : {}),
38
+ mixed: !!parent && !!settings.anthropicInstance && parent !== settings.anthropicInstance, available };
39
+ if (settings.backend !== "anthropic") return base;
40
+ try {
41
+ const resolved = resolveAnthropicHelperInstance(config, { backendFamily: "anthropic",
42
+ anthropicInstance: settings.anthropicInstance, parentProviderName });
43
+ const legacy = resolved === undefined ? resolveSidecarAuth(config).anthropicProviderName : resolved;
44
+ return { ...base, ...(isAnthropicInstanceId(legacy) ? { resolved: legacy } : {}) };
45
+ } catch (error) {
46
+ if (!(error instanceof AnthropicHelperUnavailableError)) throw error;
47
+ return { ...base, code: error.code };
48
+ }
49
+ }
50
+
51
+ /** An unavailable explicit pool removes its executor; it never resumes legacy discovery. */
52
+ export function sidecarOptionsAuth(config: OcxConfig, instance?: AnthropicInstanceId): SidecarAuthState {
53
+ try { return resolveSidecarAuth(config, instance); }
54
+ catch (error) {
55
+ if (!(error instanceof AnthropicHelperUnavailableError)) throw error;
56
+ // Preserve other backend defaults while removing all Anthropic executor authority.
57
+ const otherBackends = { ...config, providers: Object.fromEntries(Object.entries(config.providers)
58
+ .filter(([, provider]) => provider.adapter !== "anthropic")) };
59
+ return { isCodexAuth: resolveSidecarAuth(otherBackends).isCodexAuth, isAnthropicAuth: false };
60
+ }
61
+ }
62
+
63
+ /** Apply only the helper identity fields to a typed, isolated validation snapshot. */
64
+ export function sidecarSettingsAfterPatch<T extends { backend?: string; model?: string; anthropicInstance?: AnthropicInstanceId }>(
65
+ stored: T | undefined, patch: Record<string, unknown>,
66
+ ): T {
67
+ const next = { ...stored } as T;
68
+ for (const key of ["backend", "model", "anthropicInstance"] as const) {
69
+ const value = patch[key];
70
+ if (value === null || (key === "model" && value === "")) delete next[key];
71
+ else if (value !== undefined) Object.assign(next, { [key]: value });
72
+ }
73
+ return next;
74
+ }
75
+
27
76
  export interface WebSearchCandidateRow extends SidecarCandidate {
28
77
  /** Executor backend that admitted this exact candidate row. */
29
78
  backend: WebSearchBackend;
@@ -41,7 +90,7 @@ export interface WebSearchModelOption {
41
90
 
42
91
  /** The candidate rows the web-search executors can actually run right now. */
43
92
  export async function webSearchCandidateRows(config: OcxConfig): Promise<WebSearchCandidateRow[]> {
44
- const auth = resolveSidecarAuth(config);
93
+ const auth = sidecarOptionsAuth(config, config.webSearchSidecar?.anthropicInstance);
45
94
  const all = await pickerVisibleSidecarCandidates(config, auth);
46
95
  return webSearchSidecarCandidates(config, auth, all).flatMap(candidate => {
47
96
  const descriptor = WEB_SEARCH_BACKENDS.find(entry =>
@@ -279,6 +279,8 @@ export interface ManagementSessionControl {
279
279
  isCurrent(req: Request, config: OcxConfig): boolean;
280
280
  /** Prove that the current browser session came from the operator-mediated pairing flow. */
281
281
  isPaired(req: Request, config: OcxConfig): boolean;
282
+ /** Revalidate independent session authority before disclosing stored data keys. */
283
+ canRevealDataKeys?(req: Request, config: OcxConfig): boolean;
282
284
  }
283
285
 
284
286
  export function createManagementSessionControl(state: ManagementAuthState): ManagementSessionControl {
@@ -307,6 +309,10 @@ export function createManagementSessionControl(state: ManagementAuthState): Mana
307
309
  isPaired(req: Request, config: OcxConfig): boolean {
308
310
  return currentSession(req, config)?.issuance === "pairing";
309
311
  },
312
+ canRevealDataKeys(req: Request, config: OcxConfig): boolean {
313
+ const issuance = currentSession(req, config)?.issuance;
314
+ return issuance === "pairing" || issuance === "tailscale-identity";
315
+ },
310
316
  revokeCurrent(req: Request): boolean {
311
317
  if (!state.available) return false;
312
318
  const credential = requestManagementCredential(req);
@@ -13,6 +13,7 @@ import { credentialDomainFor } from "../protocols/opaque-state";
13
13
  import { resolveProtocolSettings } from "../protocols/settings";
14
14
  import type { RouteResult } from "../router";
15
15
  import type { OcxConfig } from "../types";
16
+ import { configuredAnthropicInstance } from "../providers/anthropic-instance";
16
17
  import { requiresVisionPreprocessing } from "../vision";
17
18
  import { resolvePinnedEffort } from "./effort-policy";
18
19
 
@@ -94,7 +95,7 @@ function bridgeOnlyPolicyApplies(
94
95
  /**
95
96
  * The credential rule. A proxy-managed key is native since PF-08. An Anthropic OAuth account
96
97
  * is native only with `managedMessagesNativeOAuth` on (itself effective only with
97
- * `managedMessagesNative`), only for the `anthropic` provider the OAuth store serves, and only
98
+ * `managedMessagesNative`), only for a configured Anthropic OAuth instance, and only
98
99
  * to `api.anthropic.com`. Pool selection and recovery are resolved at native dispatch. `forward`
99
100
  * belongs to the caller.
100
101
  */
@@ -107,7 +108,7 @@ function credentialDeclineReason(
107
108
  if (provider.authMode === undefined || provider.authMode === "key") return undefined;
108
109
  if (provider.authMode !== "oauth") return "auth-mode-not-native";
109
110
  if (!resolveProtocolSettings(config, route.providerName).rollout.managedMessagesNativeOAuth) return "auth-mode-not-native";
110
- if (route.providerName !== "anthropic") return "auth-mode-not-native";
111
+ if (!configuredAnthropicInstance(config, route.providerName)) return "auth-mode-not-native";
111
112
  if (!credentialDomainFor(provider)?.firstPartyAnthropic) return "auth-mode-not-native";
112
113
  return undefined;
113
114
  }
@@ -19,17 +19,11 @@
19
19
  * No token, account id or body content is logged or returned in an error message.
20
20
  */
21
21
  import type { OAuthAccessSnapshot } from "../oauth";
22
- import {
23
- commitAnthropicSelectionRouting,
24
- captureAnthropicManualSelectionGeneration,
25
- getAnthropicPoolAccessSnapshot,
26
- getAnthropicAccountHealthSnapshot,
27
- resolveAnthropicDispatchAccountId,
28
- resolveAnthropicAccountForSession,
29
- getEligibleAnthropicAccounts,
30
- isAnthropicAccountPoolEnabled,
31
- } from "../oauth/anthropic-routing";
32
- import { resolveAnthropicModelRoute, routeCandidates, type AnthropicRouteDecision } from "../oauth/anthropic-model-routes";
22
+ import { anthropicRoutingFor } from "../oauth/anthropic-routing";
23
+ import { resolveAnthropicModelRouteForInstance, routeCandidates, type AnthropicRouteDecision } from "../oauth/anthropic-model-routes";
24
+ import { configuredAnthropicInstance, type AnthropicInstanceId } from "../providers/anthropic-instance";
25
+ import { resolveAnthropicMessagesUrl } from "../adapters/anthropic";
26
+ import { routedProviderConfig } from "../router";
33
27
  import {
34
28
  captureOAuthAccountSelection,
35
29
  commitOAuthAccountSelection,
@@ -40,7 +34,6 @@ import type { TranslatorBudget } from "../lib/translator-budget";
40
34
  import type { OcxConfig } from "../types";
41
35
  import { relaySseWithPayloadRewrite } from "./sse-payload-rewrite";
42
36
 
43
- const PROVIDER = "anthropic";
44
37
  const MAX_SELECTION_ATTEMPTS = 3;
45
38
 
46
39
  type Selection = NonNullable<ReturnType<typeof captureOAuthAccountSelection>>;
@@ -52,6 +45,9 @@ function isRec(value: unknown): value is Rec {
52
45
 
53
46
  /** The committed selection and the credential snapshot one native request is served with. */
54
47
  export interface NativeOAuthBinding {
48
+ readonly instance: AnthropicInstanceId;
49
+ /** Config-authorized physical destination, captured before any credential await. */
50
+ readonly routeTarget: string;
55
51
  readonly selection: Selection;
56
52
  readonly snapshot: OAuthAccessSnapshot;
57
53
  /** Anthropic account UUID from the same stored generation, distinct from snapshot.accountId. */
@@ -74,17 +70,43 @@ export class NativeOAuthSelectionChangedError extends Error {
74
70
  /** Invalid operator route configuration remains a request refusal, not an auth failure. */
75
71
  export class NativeOAuthModelRouteError extends Error {}
76
72
 
77
- function currentRoute(config: OcxConfig, model?: string): AnthropicRouteDecision | null {
78
- const resolved = model ? resolveAnthropicModelRoute(config, model) : { decision: null };
73
+ /**
74
+ * Current configured ownership and target; a revoked B never resolves orphan credentials.
75
+ *
76
+ * Adapter, auth mode and destination are read from the routed provider, the same normalization the
77
+ * settled route used to compute its target. Routing canonicalizes the primary row's adapter and auth
78
+ * mode from the registry, so a raw-field comparison would refuse an existing Pool 1 row the route
79
+ * already accepted. B's own raw-row shape is still enforced through `configuredAnthropicInstance`.
80
+ */
81
+ function nativeOAuthRouteTarget(instance: AnthropicInstanceId, config: OcxConfig): string {
82
+ const provider = config.providers[instance];
83
+ if (configuredAnthropicInstance(config, instance) !== instance || !provider || provider.disabled === true) {
84
+ throw new NativeOAuthSelectionChangedError();
85
+ }
86
+ let routed: ReturnType<typeof routedProviderConfig>;
87
+ try {
88
+ routed = routedProviderConfig(instance, provider);
89
+ } catch {
90
+ // A destination the router now refuses is a moved route, not a different failure class.
91
+ throw new NativeOAuthSelectionChangedError();
92
+ }
93
+ if (routed.authMode !== "oauth" || routed.adapter !== "anthropic") throw new NativeOAuthSelectionChangedError();
94
+ return resolveAnthropicMessagesUrl(routed);
95
+ }
96
+
97
+ function currentRoute(instance: AnthropicInstanceId, config: OcxConfig, model?: string): AnthropicRouteDecision | null {
98
+ const resolved = model ? resolveAnthropicModelRouteForInstance(instance, config, model) : { decision: null };
79
99
  if (resolved.error) throw new NativeOAuthModelRouteError(`Invalid Anthropic model routes: ${resolved.error}`);
80
100
  return resolved.decision ? { ...resolved.decision, accounts: [...resolved.decision.accounts] } : null;
81
101
  }
82
102
 
83
- function routeIsCurrent(config: OcxConfig, model: string | undefined, decision: AnthropicRouteDecision | null): boolean {
84
- return JSON.stringify(currentRoute(config, model)) === JSON.stringify(decision);
103
+ function routeIsCurrent(instance: AnthropicInstanceId, config: OcxConfig, model: string | undefined, decision: AnthropicRouteDecision | null): boolean {
104
+ return JSON.stringify(currentRoute(instance, config, model)) === JSON.stringify(decision);
85
105
  }
86
106
 
87
107
  export interface NativeOAuthBindingOptions {
108
+ /** Settled route destination; retries may never authorize a replacement destination. */
109
+ routeTarget?: string;
88
110
  sessionKey?: string | null;
89
111
  model?: string;
90
112
  /** An account proposed by the shared pre-output refusal recovery policy. */
@@ -94,21 +116,34 @@ export interface NativeOAuthBindingOptions {
94
116
  expectedRecoveryRouteDecision?: AnthropicRouteDecision | null;
95
117
  }
96
118
 
119
+ /** Compatibility entrypoint: the original instance only. */
120
+ export function resolveNativeOAuthBinding(config: OcxConfig, options: NativeOAuthBindingOptions = {}): Promise<NativeOAuthBinding> {
121
+ return resolveNativeOAuthBindingForInstance("anthropic", config, options);
122
+ }
123
+
97
124
  /** Resolve the shared pool selector and commit its exact credential generation for dispatch. */
98
- export async function resolveNativeOAuthBinding(
125
+ export async function resolveNativeOAuthBindingForInstance(
126
+ instance: AnthropicInstanceId,
99
127
  config: OcxConfig,
100
128
  options: NativeOAuthBindingOptions = {},
101
129
  ): Promise<NativeOAuthBinding> {
130
+ const routeTarget = options.routeTarget ?? nativeOAuthRouteTarget(instance, config);
131
+ const routing = anthropicRoutingFor(instance);
132
+ const assertOwner = () => {
133
+ if (nativeOAuthRouteTarget(instance, config) !== routeTarget) throw new NativeOAuthSelectionChangedError();
134
+ };
102
135
  const sessionKey = options.sessionKey ?? null;
103
136
  const model = options.model;
104
137
  for (let attempt = 0; attempt < MAX_SELECTION_ATTEMPTS; attempt++) {
105
- const routeDecision = currentRoute(config, model);
106
- const manualSelectionGeneration = captureAnthropicManualSelectionGeneration();
107
- const selection = captureOAuthAccountSelection(PROVIDER);
138
+ assertOwner();
139
+ const routeDecision = currentRoute(instance, config, model);
140
+ const manualSelectionGeneration = routing.captureAnthropicManualSelectionGeneration();
141
+ const selection = captureOAuthAccountSelection(instance);
108
142
  // Always ask admission first so pause/login/cooldown retains its typed local refusal.
109
- const admittedId = await resolveAnthropicDispatchAccountId(config, sessionKey, routeDecision, model);
110
- if (!routeIsCurrent(config, model, routeDecision)) continue;
111
- const proposed = resolveAnthropicAccountForSession(sessionKey, config, Date.now(), routeDecision, model);
143
+ const admittedId = await routing.resolveAnthropicDispatchAccountId(config, sessionKey, routeDecision, model);
144
+ assertOwner();
145
+ if (!routeIsCurrent(instance, config, model, routeDecision)) continue;
146
+ const proposed = routing.resolveAnthropicAccountForSession(sessionKey, config, Date.now(), routeDecision, model);
112
147
  // A lost selection commit discards a recovery proposal: a newer manual/policy choice wins.
113
148
  const recoverySelectionMatches = !!options.expectedRecoverySelection
114
149
  && selection?.accountId === options.expectedRecoverySelection.accountId
@@ -118,27 +153,30 @@ export async function resolveNativeOAuthBinding(
118
153
  const accountId = attempt === 0 && options.candidateAccountId && recoverySelectionMatches && recoveryRouteMatches
119
154
  ? options.candidateAccountId : admittedId;
120
155
  if (!selection) continue;
121
- if (!routeCandidates(getEligibleAnthropicAccounts(Date.now(), model), routeDecision).includes(accountId)) {
156
+ if (!routeCandidates(routing.getEligibleAnthropicAccounts(Date.now(), model), routeDecision).includes(accountId)) {
122
157
  throw new NativeOAuthSelectionChangedError();
123
158
  }
124
- const candidate = await getAnthropicPoolAccessSnapshot(accountId);
125
- if (!routeIsCurrent(config, model, routeDecision)) continue;
126
- const committed = await commitOAuthAccountSelection(PROVIDER, candidate.accountId, {
159
+ const candidate = await routing.getAnthropicPoolAccessSnapshot(accountId);
160
+ assertOwner();
161
+ if (candidate.provider !== instance) throw new NativeOAuthSelectionChangedError();
162
+ if (!routeIsCurrent(instance, config, model, routeDecision)) continue;
163
+ const committed = await commitOAuthAccountSelection(instance, candidate.accountId, {
127
164
  expectedSelection: selection,
128
165
  expectedCredentialGeneration: candidate.generation,
129
166
  requireUsableAccount: true,
130
167
  });
168
+ assertOwner();
131
169
  if (committed) {
132
- if (!routeIsCurrent(config, model, routeDecision)
133
- || manualSelectionGeneration !== captureAnthropicManualSelectionGeneration()) continue;
134
- if (!commitAnthropicSelectionRouting(candidate.accountId, selection, committed, {
170
+ if (!routeIsCurrent(instance, config, model, routeDecision)
171
+ || manualSelectionGeneration !== routing.captureAnthropicManualSelectionGeneration()) continue;
172
+ if (!routing.commitAnthropicSelectionRouting(candidate.accountId, selection, committed, {
135
173
  config, sessionKey, model, routeDecision,
136
174
  reason: accountId === proposed.accountId ? proposed.reason : undefined,
137
175
  expectedCredentialGeneration: candidate.generation,
138
176
  })) continue;
139
- const row = getAccountCredentialWithStatus(PROVIDER, candidate.accountId);
140
- if (!row || credentialGeneration(row.credential) !== candidate.generation) continue;
141
- const binding = { selection: committed, snapshot: candidate, providerAccountUuid: row.credential.accountId, routeDecision, sessionKey, model, config, manualSelectionGeneration };
177
+ const row = getAccountCredentialWithStatus(instance, candidate.accountId);
178
+ if (!row || row.credential.access !== candidate.accessToken || credentialGeneration(row.credential) !== candidate.generation) continue;
179
+ const binding: NativeOAuthBinding = { instance, routeTarget, selection: committed, snapshot: candidate, providerAccountUuid: row.credential.accountId, routeDecision, sessionKey, model, config, manualSelectionGeneration };
142
180
  if (nativeOAuthBindingIsCurrent(binding)) return binding;
143
181
  }
144
182
  }
@@ -150,12 +188,18 @@ export async function resolveNativeOAuthBinding(
150
188
  * selection, plus the same usable credential generation. Checked before every physical send.
151
189
  */
152
190
  export function nativeOAuthBindingIsCurrent(binding: NativeOAuthBinding): boolean {
153
- if (binding.manualSelectionGeneration !== captureAnthropicManualSelectionGeneration()
154
- || !routeIsCurrent(binding.config, binding.model, binding.routeDecision)) return false;
155
- const selected = captureOAuthAccountSelection(PROVIDER);
156
- const row = getAccountCredentialWithStatus(PROVIDER, binding.snapshot.accountId);
157
- const sessionRoute = isAnthropicAccountPoolEnabled(binding.config) && binding.sessionKey
158
- ? resolveAnthropicAccountForSession(binding.sessionKey, binding.config, Date.now(), binding.routeDecision, binding.model) : null;
191
+ const { instance } = binding;
192
+ const routing = anthropicRoutingFor(instance);
193
+ try {
194
+ if (nativeOAuthRouteTarget(instance, binding.config) !== binding.routeTarget) return false;
195
+ } catch { return false; }
196
+ if (binding.snapshot.provider !== instance) return false;
197
+ if (binding.manualSelectionGeneration !== routing.captureAnthropicManualSelectionGeneration()
198
+ || !routeIsCurrent(instance, binding.config, binding.model, binding.routeDecision)) return false;
199
+ const selected = captureOAuthAccountSelection(instance);
200
+ const row = getAccountCredentialWithStatus(instance, binding.snapshot.accountId);
201
+ const sessionRoute = routing.isAnthropicAccountPoolEnabled(binding.config) && binding.sessionKey
202
+ ? routing.resolveAnthropicAccountForSession(binding.sessionKey, binding.config, Date.now(), binding.routeDecision, binding.model) : null;
159
203
  // Another conversation may move the automatic active pointer without revoking this
160
204
  // session's affinity. Manual selection clears affinity and precedes it in the selector.
161
205
  const selectionCurrent = sessionRoute?.reason === "affinity"
@@ -163,8 +207,9 @@ export function nativeOAuthBindingIsCurrent(binding: NativeOAuthBinding): boolea
163
207
  || (selected?.accountId === binding.selection.accountId && selected?.revision === binding.selection.revision);
164
208
  return selectionCurrent
165
209
  && !!row && !row.paused && !row.needsReauth && row.credential.expires > Date.now()
166
- && !getAnthropicAccountHealthSnapshot(binding.snapshot.accountId)
167
- && routeCandidates(getEligibleAnthropicAccounts(Date.now(), binding.model), binding.routeDecision).includes(binding.snapshot.accountId)
210
+ && !routing.getAnthropicAccountHealthSnapshot(binding.snapshot.accountId)
211
+ && routeCandidates(routing.getEligibleAnthropicAccounts(Date.now(), binding.model), binding.routeDecision).includes(binding.snapshot.accountId)
212
+ && row.credential.access === binding.snapshot.accessToken
168
213
  && credentialGeneration(row.credential) === binding.snapshot.generation
169
214
  && row.credential.accountId === binding.providerAccountUuid;
170
215
  }
@@ -36,7 +36,10 @@ export function nativeMessagesToolScopeDenial(
36
36
  const definition = tool.definition;
37
37
  if (typeof definition.name !== "string") { anonymous.push(definition); continue; }
38
38
  // A temporary replacement keeps the declaration it shadows in scope as well.
39
- definitions.set(definition.name, permanent ? [definition] : [...definitions.get(definition.name) ?? [], definition]);
39
+ // These arrays belong to this check, not to the caller's declarations.
40
+ const declarations = permanent ? [] : definitions.get(definition.name) ?? [];
41
+ declarations.push(definition);
42
+ definitions.set(definition.name, declarations);
40
43
  withdrawn.delete(definition.name);
41
44
  } else if (tool.type === "tool_reference" && typeof tool.name === "string") {
42
45
  if (block.type === "tool_removal" && permanent) withdrawn.add(tool.name);
@@ -0,0 +1,39 @@
1
+ /** Pure caller-forward exclusion shared by Messages ingress and protocol preview. */
2
+ import { resolveInboundModel } from "../claude/inbound-model-options";
3
+ import { routeConcreteModel } from "../router";
4
+ import { configuredAnthropicInstance } from "../providers/anthropic-instance";
5
+ import { hasOwnProvider } from "../config/provider-name";
6
+ import type { OcxConfig } from "../types";
7
+
8
+ export function messagesSelectorTargetsSecondaryInstance(
9
+ config: OcxConfig,
10
+ model: string,
11
+ cc: OcxConfig["claudeCode"] = config.claudeCode,
12
+ ): boolean {
13
+ const selector = resolveInboundModel(model, cc);
14
+ const slash = selector.indexOf("/");
15
+ if (slash <= 0) return false;
16
+ const qualifier = selector.slice(0, slash);
17
+ // An orphan or unmarked B selector must also never forward a caller's credential.
18
+ if (qualifier === "anthropic2") return true;
19
+ // Provider keys are case-sensitive and outrank aliases, including when disabled.
20
+ if (hasOwnProvider(config.providers, qualifier)) return false;
21
+ try {
22
+ // Concrete resolution handles configured provider aliases without picking a combo/account.
23
+ return routeConcreteModel(config, selector).providerName === "anthropic2";
24
+ } catch {
25
+ return config.providers.anthropic2?.alias?.trim().toLowerCase() === qualifier.toLowerCase();
26
+ }
27
+ }
28
+
29
+ /** A missing B row must not fall through the router's generic default-provider path. */
30
+ export function messagesSecondaryInstanceUnavailable(
31
+ config: OcxConfig,
32
+ model: string,
33
+ cc: OcxConfig["claudeCode"] = config.claudeCode,
34
+ ): boolean {
35
+ if (!messagesSelectorTargetsSecondaryInstance(config, model, cc)) return false;
36
+ const provider = config.providers.anthropic2;
37
+ return !provider || provider.disabled === true
38
+ || provider.authMode === "oauth" && configuredAnthropicInstance(config, "anthropic2") !== "anthropic2";
39
+ }