@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,7 +1,8 @@
1
- import { readConfigFileSnapshot, validateConfigCandidate } from "../../config/diagnostics";
2
- import { ConfigWritePublishedError } from "../../config/persist-unlocked";
1
+ import { isAnthropicOAuthInstance, configuredAnthropicInstance, type AnthropicInstanceId } from "../../providers/anthropic-instance";
2
+ import { resolveAnthropicAccountPoolConfig } from "../../oauth/anthropic-pool-config";
3
+ import { persistAnthropicPoolPatch, writeAnthropicPoolSettings } from "./anthropic-pool-settings";
3
4
  import { parseAnthropicModelRoutes, readAnthropicModelRoutes } from "../../oauth/anthropic-model-routes";
4
- import { effectiveAnthropicAccountThreshold } from "../../oauth/anthropic-account-threshold";
5
+ import { effectiveAnthropicAccountThresholdForInstance } from "../../oauth/anthropic-account-threshold";
5
6
  import { handleAnthropicAccountThreshold } from "./anthropic-account-threshold";
6
7
  import { randomBytes, randomUUID } from "node:crypto";
7
8
  import { readFileSync } from "node:fs";
@@ -18,7 +19,6 @@ import {
18
19
  readConfigDiagnostics,
19
20
  reconcileLiveConfigFromDisk,
20
21
  saveConfigPreservingClaudeCode,
21
- mutatePersistedConfig,
22
22
  } from "../../config";
23
23
  import {
24
24
  clearLoginState,
@@ -39,7 +39,7 @@ import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/ke
39
39
  import { deriveProviderPresets } from "../../providers/derive";
40
40
  import { providerCodexAccountMode } from "../../providers/registry";
41
41
  import { routedSlug, slugEquals } from "../../providers/slug-codec";
42
- import { clearAccountQuotaCache, clearProviderQuotaCache, fetchProviderAccountQuotas, fetchProviderApiKeyQuotas, fetchProviderQuotaReports, providerOAuthAccountQuotaMode, providerApiKeyQuotaMode, readPassiveProviderAccountQuotas } from "../../providers/quota";
42
+ import { clearAccountQuotaCache, clearProviderQuotaCache, clearProviderQuotaCacheFor, fetchProviderAccountQuotas, fetchProviderApiKeyQuotas, fetchProviderQuotaReports, providerOAuthAccountQuotaMode, providerApiKeyQuotaMode, readPassiveProviderAccountQuotas } from "../../providers/quota";
43
43
  import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
44
44
  import { clearThreadAccountMap } from "../../codex/routing";
45
45
  import {
@@ -101,6 +101,21 @@ function isDevinCloudDirectProvider(provider: string): boolean {
101
101
  return provider === "devin" || provider === "devin-cli";
102
102
  }
103
103
 
104
+ /**
105
+ * Quota invalidation after an account mutation. A B mutation retires only B's provider-level
106
+ * report, routing row and per-account rows; A's cache, epochs and in-flight probes survive.
107
+ * Other providers keep the global clear; `accountRows` also drops that provider's account rows.
108
+ */
109
+ function clearQuotaCachesAfterAccountMutation(provider: string, accountRows: boolean): void {
110
+ if (provider === "anthropic2") {
111
+ clearProviderQuotaCacheFor(provider);
112
+ clearAccountQuotaCache(provider);
113
+ return;
114
+ }
115
+ clearProviderQuotaCache();
116
+ if (accountRows) clearAccountQuotaCache(provider);
117
+ }
118
+
104
119
  async function clearDevinCloudDirectCaches(): Promise<void> {
105
120
  const { clearCachedUserJwt, clearCachedCatalog } = await import("../../adapters/devin/cloud-direct");
106
121
  clearCachedUserJwt();
@@ -208,49 +223,12 @@ function metaMuseConsentRequired(provider: string, principal: ManagementContext[
208
223
 
209
224
  function genericOAuthProviderConfig(provider: string, config: ManagementContext["config"]) {
210
225
  const configured = config.providers[provider];
226
+ if (provider === "anthropic2" && !configuredAnthropicInstance(config, provider)) return undefined;
211
227
  if (configured) return configured;
212
228
  const definition = OAUTH_PROVIDERS[provider];
213
229
  return definition?.resolveProviderConfig?.(config) ?? definition?.providerConfig;
214
230
  }
215
231
 
216
- /** Both Anthropic writers share publication-aware recovery from the atomic mutation owner. */
217
- function persistAnthropicPoolPatch(
218
- config: OcxConfig,
219
- patch: (target: OcxConfig) => { changed: boolean; value: NonNullable<OcxConfig["anthropicAccountPool"]> },
220
- ): { status: "saved"; warning?: "config_bookkeeping_failed" } | { status: "failed"; response: Response } {
221
- const before = readConfigFileSnapshot();
222
- const unknown = () => ({ status: "failed" as const, response: jsonResponse({
223
- error: "Pool settings save state is unknown; reload settings before editing again",
224
- code: "config_save_state_unknown",
225
- }, 409) });
226
- try {
227
- const saved = mutatePersistedConfig(patch);
228
- if (saved.status === "unavailable") return unknown();
229
- config.anthropicAccountPool = saved.value;
230
- reconcileLiveStateStores();
231
- return { status: "saved" };
232
- } catch (error) {
233
- const persisted = readConfigFileSnapshot();
234
- if (persisted.diagnostics.source !== "file" || persisted.raw === undefined) return unknown();
235
- // File reads may salvage hand edits; recovery success requires a strict, authoritative document.
236
- let validated: ReturnType<typeof validateConfigCandidate>;
237
- try { validated = validateConfigCandidate(JSON.parse(persisted.raw.replace(/^\uFEFF/, ""))); }
238
- catch { return unknown(); }
239
- if (!validated.ok) return unknown();
240
- const current = validated.config;
241
- const expected = patch(structuredClone(current)).value;
242
- const matches = JSON.stringify(current.anthropicAccountPool) === JSON.stringify(expected);
243
- if (matches) {
244
- config.anthropicAccountPool = current.anthropicAccountPool;
245
- // A failed reconcile must not turn confirmed publication back into a rollback.
246
- try { reconcileLiveStateStores(); } catch { /* Fixed warning covers bookkeeping failure. */ }
247
- return { status: "saved", warning: "config_bookkeeping_failed" };
248
- }
249
- if (error instanceof ConfigWritePublishedError || persisted.raw !== before.raw) return unknown();
250
- return { status: "failed", response: jsonResponse({ error: "Pool settings could not be saved" }, 500) };
251
- }
252
- }
253
-
254
232
  export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<Response | null> {
255
233
  const { req, url, config, deps, principal, syncClaudeAgentDefsBestEffort } = ctx;
256
234
 
@@ -422,9 +400,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
422
400
  clearModelCache(provider);
423
401
  clearGatherRoutedModelsInflight();
424
402
  // Drop cached/last-good quota rows tied to the removed credential.
425
- const { clearProviderQuotaCache, clearAccountQuotaCache } = await import("../../providers/quota");
426
- clearProviderQuotaCache();
427
- clearAccountQuotaCache(provider);
403
+ clearQuotaCachesAfterAccountMutation(provider, true);
428
404
  // The cached user_jwt's payload contains the api_key, and the catalog is
429
405
  // keyed by that key. Without this they outlive the credential in process
430
406
  // memory until the JWT's own ~24 minute expiry. `devin-cli` is a deprecated
@@ -445,7 +421,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
445
421
  const { isGenericFailoverProvider, kiroAutoSelection } = await import("../../oauth/generic-account-failover");
446
422
  const effectiveProvider = genericOAuthProviderConfig(provider, config);
447
423
  const supportsPause = effectiveProvider !== undefined
448
- && (provider === "anthropic" && effectiveProvider.authMode === "oauth"
424
+ && (isAnthropicOAuthInstance(provider) && effectiveProvider.authMode === "oauth"
449
425
  || isGenericFailoverProvider(provider, effectiveProvider));
450
426
  const {
451
427
  oauthAccountHealthFields,
@@ -456,7 +432,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
456
432
  const set = getAccountSet(provider);
457
433
  const current = getLoginStatus(provider, emailMaskingEnabled(config));
458
434
  return {
459
- activeAccountId: current.activeAccountId ?? null,
435
+ ...(provider === "anthropic2" ? { provider } : {}), activeAccountId: current.activeAccountId ?? null,
460
436
  accounts: (current.accounts ?? []).map(summary => {
461
437
  const full = set?.accounts.find(account => account.id === summary.id);
462
438
  const health = full
@@ -467,9 +443,9 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
467
443
  });
468
444
  return { ...summary, ...oauthAccountHealthFields(provider, summary.id, health), quotaMode,
469
445
  ...(supportsPause ? { paused: full?.paused === true } : {}),
470
- ...(provider === "anthropic" && supportsPause ? { autoSwitchThresholdOverride: full?.autoSwitchThresholdOverride ?? null,
471
- effectiveAutoSwitchThreshold: effectiveAnthropicAccountThreshold(config, full),
472
- autoSwitchThreshold: effectiveAnthropicAccountThreshold(config) } : {}),
446
+ ...(isAnthropicOAuthInstance(provider) && supportsPause ? { autoSwitchThresholdOverride: full?.autoSwitchThresholdOverride ?? null,
447
+ effectiveAutoSwitchThreshold: effectiveAnthropicAccountThresholdForInstance(provider as AnthropicInstanceId, config, full),
448
+ autoSwitchThreshold: effectiveAnthropicAccountThresholdForInstance(provider as AnthropicInstanceId, config) } : {}),
473
449
  ...(provider === "kiro" && full ? kiroAutoSelection(full) : {}) };
474
450
  }),
475
451
  };
@@ -489,11 +465,11 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
489
465
  // from the post-probe store so the response is not stale.
490
466
  const rows = passiveQuota
491
467
  ? readPassiveProviderAccountQuotas(provider)
492
- : await fetchProviderAccountQuotas(provider, forceRefresh, quotaProvider);
468
+ : await fetchProviderAccountQuotas(provider, forceRefresh, quotaProvider, config);
493
469
  const byId = new Map(rows.map(row => [row.accountId, row]));
494
470
  const projected = projectAccounts();
495
471
  return jsonResponse({
496
- activeAccountId: projected.activeAccountId,
472
+ ...(provider === "anthropic2" ? { provider } : {}), activeAccountId: projected.activeAccountId,
497
473
  accounts: projected.accounts.map(account => {
498
474
  const row = byId.get(account.id);
499
475
  if (!row) return account;
@@ -514,6 +490,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
514
490
  const body = await readManagementJsonBodyOr(req, {}) as { provider?: string; accountId?: string };
515
491
  const provider = (body.provider ?? "").trim().toLowerCase();
516
492
  if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
493
+ if (provider === "anthropic2" && !configuredAnthropicInstance(config, provider)) return jsonResponse({ error: "Anthropic pool instance is not configured" }, 409);
517
494
  if (!body.accountId) return jsonResponse({ error: "missing accountId" }, 400);
518
495
  const { getAccountCredentialWithStatus, setActiveAccount } = await import("../../oauth/store");
519
496
  const current = getAccountCredentialWithStatus(provider, body.accountId);
@@ -532,16 +509,15 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
532
509
  // side carries resetCodexRoutingForManualSelection for.
533
510
  const { genericPoolKey, seedPoolRotationAccount } = await import("../../oauth/pool-kernel");
534
511
  seedPoolRotationAccount(genericPoolKey(provider), body.accountId);
535
- if (provider === "anthropic") {
536
- const { resetAnthropicRoutingForManualSelection } = await import("../../oauth/anthropic-routing");
537
- resetAnthropicRoutingForManualSelection(body.accountId);
512
+ if (isAnthropicOAuthInstance(provider)) {
513
+ const { anthropicRoutingFor } = await import("../../oauth/anthropic-routing");
514
+ anthropicRoutingFor(provider).resetAnthropicRoutingForManualSelection(body.accountId);
538
515
  }
539
516
  const { clearModelCache } = await import("../../codex/model-cache");
540
517
  const { clearGatherRoutedModelsInflight } = await import("../../codex/catalog");
541
518
  clearModelCache(provider);
542
519
  clearGatherRoutedModelsInflight();
543
- const { clearProviderQuotaCache } = await import("../../providers/quota");
544
- clearProviderQuotaCache();
520
+ clearQuotaCachesAfterAccountMutation(provider, false);
545
521
  return jsonResponse({ ok: true, provider, activeAccountId: body.accountId });
546
522
  }
547
523
 
@@ -558,7 +534,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
558
534
 
559
535
  const { isGenericFailoverProvider } = await import("../../oauth/generic-account-failover");
560
536
  const effectiveProvider = genericOAuthProviderConfig(provider, config);
561
- if (!effectiveProvider || !(provider === "anthropic" && effectiveProvider.authMode === "oauth"
537
+ if (!effectiveProvider || !(isAnthropicOAuthInstance(provider) && effectiveProvider.authMode === "oauth"
562
538
  || isGenericFailoverProvider(provider, effectiveProvider))) {
563
539
  return jsonResponse({ error: "account pause is not supported for this OAuth provider" }, 400);
564
540
  }
@@ -568,9 +544,9 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
568
544
  if (result.status === "not-found") return jsonResponse({ error: "account not found" }, 404);
569
545
 
570
546
  if (result.activeAccountChanged) {
571
- if (provider === "anthropic") {
572
- const { resetAnthropicRoutingForManualSelection } = await import("../../oauth/anthropic-routing");
573
- resetAnthropicRoutingForManualSelection(result.activeAccountId);
547
+ if (isAnthropicOAuthInstance(provider)) {
548
+ const { anthropicRoutingFor } = await import("../../oauth/anthropic-routing");
549
+ anthropicRoutingFor(provider).resetAnthropicRoutingForManualSelection(result.activeAccountId);
574
550
  } else {
575
551
  const { genericPoolKey, seedPoolRotationAccount } = await import("../../oauth/pool-kernel");
576
552
  seedPoolRotationAccount(genericPoolKey(provider), result.activeAccountId);
@@ -579,8 +555,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
579
555
  const { clearGatherRoutedModelsInflight } = await import("../../codex/catalog");
580
556
  clearModelCache(provider);
581
557
  clearGatherRoutedModelsInflight();
582
- const { clearProviderQuotaCache } = await import("../../providers/quota");
583
- clearProviderQuotaCache();
558
+ clearQuotaCachesAfterAccountMutation(provider, false);
584
559
  }
585
560
 
586
561
  return jsonResponse({
@@ -671,7 +646,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
671
646
  if (autoSwitchThreshold !== undefined) config.autoSwitchThreshold = autoSwitchThreshold;
672
647
  } else if (kind === "anthropic") {
673
648
  const patchPool = (target: OcxConfig) => {
674
- const pool = { ...(target.anthropicAccountPool ?? {}) };
649
+ const pool = { ...resolveAnthropicAccountPoolConfig(target, provider as AnthropicInstanceId) };
675
650
  if (fields.nativeMessages !== undefined) pool.nativeMessages = fields.nativeMessages as boolean;
676
651
  if (fields.enabled !== undefined) pool.enabled = fields.enabled as boolean;
677
652
  if (strategy !== undefined) pool.strategy = strategy as never;
@@ -682,10 +657,10 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
682
657
  if (fields.routes === null) delete pool.routes;
683
658
  else if (parsedRoutes?.ok) pool.routes = parsedRoutes.routes;
684
659
  }
685
- target.anthropicAccountPool = pool;
660
+ writeAnthropicPoolSettings(target, provider as AnthropicInstanceId, pool);
686
661
  return { changed: true, value: pool };
687
662
  };
688
- const saved = persistAnthropicPoolPatch(config, patchPool);
663
+ const saved = persistAnthropicPoolPatch(config, provider as AnthropicInstanceId, patchPool);
689
664
  if (saved.status === "failed") return saved.response;
690
665
  return jsonResponse({ ...unifiedPoolSettingsDto(config, provider, kind), ...(saved.warning ? { warning: saved.warning } : {}) });
691
666
  } else {
@@ -710,7 +685,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
710
685
  // Opt-in Anthropic OAuth account pool (#294): enable/threshold/strategy + clear cooldown.
711
686
  if (url.pathname === "/api/oauth/accounts/pool" && req.method === "GET") {
712
687
  const provider = (url.searchParams.get("provider") ?? "").trim().toLowerCase();
713
- if (provider !== "anthropic") {
688
+ if (!isAnthropicOAuthInstance(provider)) {
714
689
  // Generic OAuth pool-settings contract (#695 slice 1): persisted per provider. `strategy`
715
690
  // and `autoSwitchThreshold` stay inert until the selector consumes them; `enabled` already
716
691
  // governs the pre-dispatch account preference. Codex keeps /api/codex-auth; api-key
@@ -722,7 +697,8 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
722
697
  }
723
698
  return jsonResponse(genericPoolSettingsDto(provider, prov, config.pool?.kernel === true));
724
699
  }
725
- const pool = config.anthropicAccountPool ?? {};
700
+ if (provider === "anthropic2" && !configuredAnthropicInstance(config, provider)) return jsonResponse({ error: "Anthropic pool instance is not configured" }, 409);
701
+ const pool = resolveAnthropicAccountPoolConfig(config, provider);
726
702
  return jsonResponse({
727
703
  provider,
728
704
  enabled: pool.enabled === true,
@@ -753,10 +729,10 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
753
729
  };
754
730
  const provider = typeof body.provider === "string" ? body.provider.trim().toLowerCase() : "";
755
731
  if (Object.hasOwn(body, "nativeMessages")) {
756
- if (provider !== "anthropic") return jsonResponse({ error: "nativeMessages is only part of the anthropic pool contract" }, 400);
732
+ if (!isAnthropicOAuthInstance(provider)) return jsonResponse({ error: "nativeMessages is only part of the anthropic pool contract" }, 400);
757
733
  if (typeof body.nativeMessages !== "boolean") return jsonResponse({ error: "nativeMessages must be a boolean" }, 400);
758
734
  }
759
- if (provider !== "anthropic") {
735
+ if (!isAnthropicOAuthInstance(provider)) {
760
736
  const {
761
737
  poolSettingsCapability, genericPoolSettingsDto, parseGenericPoolStrategy, parseGenericAutoSwitchThreshold,
762
738
  parseGenericStickyLimit, parseKiroAccountCap,
@@ -810,12 +786,13 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
810
786
  saveConfigPreservingClaudeCode(config);
811
787
  return jsonResponse({ ok: true, ...genericPoolSettingsDto(provider, prov, config.pool?.kernel === true) });
812
788
  }
813
- let enabled = config.anthropicAccountPool?.enabled === true;
789
+ if (provider === "anthropic2" && !configuredAnthropicInstance(config, provider)) return jsonResponse({ error: "Anthropic pool instance is not configured" }, 409);
790
+ let enabled = resolveAnthropicAccountPoolConfig(config, provider)?.enabled === true;
814
791
  if (body.enabled !== undefined) {
815
792
  if (typeof body.enabled !== "boolean") return jsonResponse({ error: "enabled must be a boolean" }, 400);
816
793
  enabled = body.enabled;
817
794
  }
818
- let threshold = config.anthropicAccountPool?.autoSwitchThreshold ?? 80;
795
+ let threshold = resolveAnthropicAccountPoolConfig(config, provider)?.autoSwitchThreshold ?? 80;
819
796
  if (body.autoSwitchThreshold !== undefined) {
820
797
  if (
821
798
  typeof body.autoSwitchThreshold !== "number"
@@ -827,7 +804,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
827
804
  }
828
805
  threshold = body.autoSwitchThreshold;
829
806
  }
830
- let strategy = config.anthropicAccountPool?.strategy;
807
+ let strategy = resolveAnthropicAccountPoolConfig(config, provider)?.strategy;
831
808
  if (body.strategy !== undefined) {
832
809
  const parsed = parseAccountPoolStrategy(body.strategy);
833
810
  if (parsed === null) {
@@ -835,7 +812,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
835
812
  }
836
813
  strategy = parsed;
837
814
  }
838
- let stickyLimit = config.anthropicAccountPool?.stickyLimit;
815
+ let stickyLimit = resolveAnthropicAccountPoolConfig(config, provider)?.stickyLimit;
839
816
  if (body.stickyLimit !== undefined) {
840
817
  const parsed = parseAccountPoolStickyLimit(body.stickyLimit);
841
818
  if (parsed === null) {
@@ -843,7 +820,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
843
820
  }
844
821
  stickyLimit = parsed;
845
822
  }
846
- let quotaWindow = config.anthropicAccountPool?.quotaWindow;
823
+ let quotaWindow = resolveAnthropicAccountPoolConfig(config, provider)?.quotaWindow;
847
824
  if (body.quotaWindow !== undefined) {
848
825
  const parsed = parseAccountPoolQuotaWindow(body.quotaWindow);
849
826
  if (parsed === null) {
@@ -855,7 +832,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
855
832
  ? parseAnthropicModelRoutes(body.routes) : null;
856
833
  if (parsedLegacyRoutes && !parsedLegacyRoutes.ok) return jsonResponse({ error: parsedLegacyRoutes.error }, 400);
857
834
  const legacyPatch = (target: OcxConfig) => {
858
- const pool = { ...(target.anthropicAccountPool ?? {}) };
835
+ const pool = { ...resolveAnthropicAccountPoolConfig(target, provider as AnthropicInstanceId) };
859
836
  if (body.enabled !== undefined) pool.enabled = enabled;
860
837
  else if (pool.enabled === undefined) pool.enabled = false;
861
838
  if (body.nativeMessages !== undefined) pool.nativeMessages = body.nativeMessages as boolean;
@@ -868,21 +845,21 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
868
845
  if (body.routes === null) delete pool.routes;
869
846
  else if (parsedLegacyRoutes?.ok) pool.routes = parsedLegacyRoutes.routes;
870
847
  }
871
- target.anthropicAccountPool = pool;
848
+ writeAnthropicPoolSettings(target, provider as AnthropicInstanceId, pool);
872
849
  return { changed: true, value: pool };
873
850
  };
874
- const saved = persistAnthropicPoolPatch(config, legacyPatch);
851
+ const saved = persistAnthropicPoolPatch(config, provider as AnthropicInstanceId, legacyPatch);
875
852
  if (saved.status === "failed") return saved.response;
876
853
  return jsonResponse({
877
854
  ok: true,
878
855
  provider,
879
- enabled: config.anthropicAccountPool?.enabled === true,
880
- autoSwitchThreshold: config.anthropicAccountPool?.autoSwitchThreshold ?? 80,
881
- strategy: normalizeAccountPoolStrategy(config.anthropicAccountPool?.strategy),
882
- stickyLimit: normalizeAccountPoolStickyLimit(config.anthropicAccountPool?.stickyLimit),
883
- quotaWindow: normalizeAccountPoolQuotaWindow(config.anthropicAccountPool?.quotaWindow),
884
- routes: config.anthropicAccountPool?.routes ?? null,
885
- nativeMessages: !Object.hasOwn(config.anthropicAccountPool ?? {}, "nativeMessages") || config.anthropicAccountPool?.nativeMessages === true,
856
+ enabled: resolveAnthropicAccountPoolConfig(config, provider)?.enabled === true,
857
+ autoSwitchThreshold: resolveAnthropicAccountPoolConfig(config, provider)?.autoSwitchThreshold ?? 80,
858
+ strategy: normalizeAccountPoolStrategy(resolveAnthropicAccountPoolConfig(config, provider)?.strategy),
859
+ stickyLimit: normalizeAccountPoolStickyLimit(resolveAnthropicAccountPoolConfig(config, provider)?.stickyLimit),
860
+ quotaWindow: normalizeAccountPoolQuotaWindow(resolveAnthropicAccountPoolConfig(config, provider)?.quotaWindow),
861
+ routes: resolveAnthropicAccountPoolConfig(config, provider)?.routes ?? null,
862
+ nativeMessages: !Object.hasOwn(resolveAnthropicAccountPoolConfig(config, provider), "nativeMessages") || resolveAnthropicAccountPoolConfig(config, provider)?.nativeMessages === true,
886
863
  ...(saved.warning ? { warning: saved.warning } : {}),
887
864
  experimental: true,
888
865
  });
@@ -891,10 +868,11 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
891
868
  const body = await readManagementJsonBodyOr(req, {}) as { provider?: unknown; accountId?: unknown };
892
869
  const provider = typeof body.provider === "string" ? body.provider.trim().toLowerCase() : "";
893
870
  const accountId = typeof body.accountId === "string" ? body.accountId.trim() : "";
894
- if (provider !== "anthropic") return jsonResponse({ error: "clear-cooldown is only supported for anthropic" }, 400);
871
+ if (!isAnthropicOAuthInstance(provider)) return jsonResponse({ error: "clear-cooldown is only supported for anthropic" }, 400);
872
+ if (provider === "anthropic2" && !configuredAnthropicInstance(config, provider)) return jsonResponse({ error: "Anthropic pool instance is not configured" }, 409);
895
873
  if (!accountId) return jsonResponse({ error: "missing accountId" }, 400);
896
- const { clearAnthropicAccountCooldown } = await import("../../oauth/anthropic-routing");
897
- const cleared = clearAnthropicAccountCooldown(accountId);
874
+ const { anthropicRoutingFor } = await import("../../oauth/anthropic-routing");
875
+ const cleared = anthropicRoutingFor(provider).clearAnthropicAccountCooldown(accountId);
898
876
  return jsonResponse({ ok: true, cleared });
899
877
  }
900
878
 
@@ -936,8 +914,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
936
914
  const { clearGatherRoutedModelsInflight } = await import("../../codex/catalog");
937
915
  clearModelCache(provider);
938
916
  clearGatherRoutedModelsInflight();
939
- clearProviderQuotaCache();
940
- clearAccountQuotaCache(provider);
917
+ clearQuotaCachesAfterAccountMutation(provider, true);
941
918
  }
942
919
  if (!imported.ok) return jsonResponse({ code: imported.code }, imported.status);
943
920
  return jsonResponse(imported.result);
@@ -969,19 +946,17 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
969
946
  const { removeAccount, getAccountSet } = await import("../../oauth/store");
970
947
  if (!(await removeAccount(provider, id))) return jsonResponse({ error: "account not found" }, 404);
971
948
  reconcileLiveStateStores();
972
- if (provider === "anthropic") {
973
- const { clearAnthropicAccountCooldown, clearAnthropicSessionAffinityForAccount } = await import("../../oauth/anthropic-routing");
974
- clearAnthropicAccountCooldown(id);
975
- clearAnthropicSessionAffinityForAccount(id);
949
+ if (isAnthropicOAuthInstance(provider)) {
950
+ const { anthropicRoutingFor } = await import("../../oauth/anthropic-routing");
951
+ anthropicRoutingFor(provider).clearAnthropicAccountCooldown(id);
952
+ anthropicRoutingFor(provider).clearAnthropicSessionAffinityForAccount(id);
976
953
  }
977
954
  if (!getAccountSet(provider)) clearLoginState(provider);
978
955
  const { clearModelCache } = await import("../../codex/model-cache");
979
956
  const { clearGatherRoutedModelsInflight } = await import("../../codex/catalog");
980
957
  clearModelCache(provider);
981
958
  clearGatherRoutedModelsInflight();
982
- const { clearProviderQuotaCache, clearAccountQuotaCache } = await import("../../providers/quota");
983
- clearProviderQuotaCache();
984
- clearAccountQuotaCache(provider);
959
+ clearQuotaCachesAfterAccountMutation(provider, true);
985
960
  // Same reasoning as logout. Removing the last account for a provider used to
986
961
  // leave the JWT and catalog in memory, because only the logout route cleared
987
962
  // them.
@@ -1168,7 +1143,13 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
1168
1143
  if (principal !== "gui-session") {
1169
1144
  return jsonResponse({ error: "dashboard session required" }, 403, req, config);
1170
1145
  }
1146
+ // Local page bootstrap is sufficient for the dashboard, not stored-secret disclosure.
1147
+ const authorized = () => ctx.sessionControl?.canRevealDataKeys?.(req, config) === true;
1148
+ const denied = () => jsonResponse({ error: "operator-authorized dashboard session required" }, 403, req, config);
1149
+ if (!authorized()) return denied();
1171
1150
  const body = await readJsonBody(req);
1151
+ // Body reception can outlive the session or its revocation; do not trust cached admission.
1152
+ if (!authorized()) return denied();
1172
1153
  if (!body || Object.keys(body).length !== 1 || typeof body.id !== "string" || !body.id) {
1173
1154
  return jsonResponse({ error: "invalid body" }, 400, req, config);
1174
1155
  }
@@ -0,0 +1,44 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import type { OcxProviderConfig } from "../../types";
3
+ import type { ProviderEditorProviderDTO } from "../auth-cors";
4
+ import { anthropicInstanceRowShapeMatches } from "../../providers/anthropic-instance-id";
5
+ import { assertAnthropicInstanceLoginConfig } from "../../oauth/store-anthropic-instance";
6
+
7
+ /** Preset publication may create B, but may never take over an existing custom row. */
8
+ export function anthropicInstancePublicationError(name: string, next: OcxProviderConfig, existing: OcxProviderConfig | undefined): string | undefined {
9
+ if (name === "anthropic2" && anthropicInstanceRowShapeMatches(name, next)) {
10
+ try { assertAnthropicInstanceLoginConfig({ providers: existing ? { [name]: existing } : {} }, name); }
11
+ catch { return "cannot add Pool 2 over an existing custom provider or unreadable configuration; resolve it first"; }
12
+ }
13
+ return undefined;
14
+ }
15
+
16
+ /** Full form edits omit provenance; preserve only an already owned compatible row. */
17
+ export function preserveAnthropicInstanceMarker(name: string, next: OcxProviderConfig, existing: OcxProviderConfig | undefined): void {
18
+ if (name !== "anthropic2" || Object.hasOwn(next, "anthropicOAuthInstance")) return;
19
+ if (anthropicInstanceRowShapeMatches(name, existing) && next.adapter === "anthropic" && next.authMode === "oauth") {
20
+ next.anthropicOAuthInstance = "anthropic2";
21
+ }
22
+ }
23
+
24
+ export function mergeProviderEditorRow(
25
+ persisted: OcxProviderConfig | undefined,
26
+ baseline: ProviderEditorProviderDTO | undefined,
27
+ next: ProviderEditorProviderDTO,
28
+ ): OcxProviderConfig {
29
+ const merged = structuredClone(persisted ?? {}) as Record<string, unknown>;
30
+ const fields = new Set([...Object.keys(baseline ?? {}), ...Object.keys(next)]);
31
+ for (const field of fields) {
32
+ const baselineHasField = baseline !== undefined && Object.hasOwn(baseline, field);
33
+ const nextHasField = Object.hasOwn(next, field);
34
+ if (
35
+ baselineHasField === nextHasField
36
+ && (!baselineHasField || isDeepStrictEqual(baseline[field], next[field]))
37
+ ) {
38
+ continue;
39
+ }
40
+ if (nextHasField) merged[field] = structuredClone(next[field]);
41
+ else delete merged[field];
42
+ }
43
+ return merged as unknown as OcxProviderConfig;
44
+ }
@@ -40,6 +40,7 @@ import { commitProviderPatch } from "./provider-patch-transaction";
40
40
  import { captureConfigTopLevelRollback } from "../../config/rebase-provenance";
41
41
  import { canonicalAutoReviewModelKey, mergeModelPinnedEfforts, modelPinnedEffortsConfigError, pinnedReasoningEffortConfigError } from "../../config/provider-validation";
42
42
  import { replaceProviderAccountSet } from "../../oauth/store";
43
+ import { captureModelsOAuthTarget, mayResolveModelsOAuth, modelsOAuthTargetMatches } from "../../oauth/model-discovery-auth";
43
44
  import { providerDestinationResolvedError } from "../../lib/destination-policy";
44
45
  import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
45
46
  import { ProviderOutboundPolicyError, providerOutboundGet, providerOutboundPost, providerRedirectError } from "../../lib/provider-outbound";
@@ -115,7 +116,6 @@ import {
115
116
  publicProviderBaseUrl,
116
117
  safeConfigDTO,
117
118
  type ProviderEditorConfigDTO,
118
- type ProviderEditorProviderDTO,
119
119
  } from "../auth-cors";
120
120
  import { providerCatalogCapabilityConfigError } from "./provider-capability-config";
121
121
  import { providerEmptyToolOutputConfigError } from "../../config/provider-validation";
@@ -138,6 +138,7 @@ import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostR
138
138
  import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
139
139
  import type { ManagementContext } from "./context";
140
140
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
141
+ import { anthropicInstancePublicationError, mergeProviderEditorRow, preserveAnthropicInstanceMarker } from "./provider-instance-ownership";
141
142
  import { providerTlsProfileDiagnostic } from "../../lib/provider-tls-profile";
142
143
 
143
144
  type ProviderPatchApplication =
@@ -213,28 +214,6 @@ type ProviderEditorCandidateResult =
213
214
 
214
215
  type ProviderEditorMutationValue = ProviderEditorCandidateResult;
215
216
 
216
- function mergeProviderEditorRow(
217
- persisted: OcxProviderConfig | undefined,
218
- baseline: ProviderEditorProviderDTO | undefined,
219
- next: ProviderEditorProviderDTO,
220
- ): OcxProviderConfig {
221
- const merged = structuredClone(persisted ?? {}) as Record<string, unknown>;
222
- const fields = new Set([...Object.keys(baseline ?? {}), ...Object.keys(next)]);
223
- for (const field of fields) {
224
- const baselineHasField = baseline !== undefined && Object.hasOwn(baseline, field);
225
- const nextHasField = Object.hasOwn(next, field);
226
- if (
227
- baselineHasField === nextHasField
228
- && (!baselineHasField || isDeepStrictEqual(baseline[field], next[field]))
229
- ) {
230
- continue;
231
- }
232
- if (nextHasField) merged[field] = structuredClone(next[field]);
233
- else delete merged[field];
234
- }
235
- return merged as unknown as OcxProviderConfig;
236
- }
237
-
238
217
  /** Build and validate a complete candidate without mutating the caller's snapshot. */
239
218
  function providerEditorCandidate(
240
219
  persisted: OcxConfig,
@@ -273,6 +252,8 @@ function providerEditorCandidate(
273
252
  const namespaceCollision = codexAccountNamespaceProviderCollisionError(candidate.codexAccountNamespaces, name);
274
253
  if (namespaceCollision) return { ok: false, status: 409, error: namespaceCollision, code: "provider_namespace_conflict" };
275
254
  const merged = mergeProviderEditorRow(persisted.providers[name], baseline.providers[name], publicProvider);
255
+ const markerCollision = anthropicInstancePublicationError(name, merged, persisted.providers[name]);
256
+ if (markerCollision) return { ok: false, status: 409, error: markerCollision, code: "anthropic_instance_collision" };
276
257
  const transportCandidate = providerTransportValidationCandidate(merged as unknown as Record<string, unknown>);
277
258
  // The editor merges onto the persisted row, so stored operator overlays (selectedModels,
278
259
  // disabled, …) ride along in the candidate. They are owned by their own write boundaries;
@@ -1172,6 +1153,9 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1172
1153
  const aliasOwnershipError = providerAliasOverlayOwnershipError(body.provider, existing);
1173
1154
  if (aliasOwnershipError) return jsonResponse({ error: aliasOwnershipError }, 400);
1174
1155
  const transportCandidate = providerTransportValidationCandidate(body.provider);
1156
+ preserveAnthropicInstanceMarker(name, transportCandidate as unknown as OcxProviderConfig, existing);
1157
+ const markerCollision = anthropicInstancePublicationError(name, transportCandidate as unknown as OcxProviderConfig, existing);
1158
+ if (markerCollision) return jsonResponse({ error: markerCollision }, 409);
1175
1159
  const pinError = applyProviderPinFields(transportCandidate as unknown as OcxProviderConfig, body.provider, existing);
1176
1160
  if (pinError) return jsonResponse({ error: pinError }, 400);
1177
1161
  const providerError = providerManagementConfigError(name, transportCandidate)
@@ -1328,6 +1312,9 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1328
1312
  // DNS validation above awaits. Re-read the live row so a dedicated alias write that
1329
1313
  // completed during that wait remains authoritative instead of being overwritten by the
1330
1314
  // older ownership snapshot used to admit this POST.
1315
+ const lateMarkerCollision = anthropicInstancePublicationError(name, prov, config.providers[name]);
1316
+ if (lateMarkerCollision) return jsonResponse({ error: lateMarkerCollision }, 409);
1317
+ preserveAnthropicInstanceMarker(name, prov, config.providers[name]);
1331
1318
  restorePersistedAliasOverlays(prov, config.providers[name]);
1332
1319
  const capabilities = Object.hasOwn(body.provider, "modelCapabilities")
1333
1320
  ? mergeModelCapabilities(undefined, prov.modelCapabilities)
@@ -1371,6 +1358,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1371
1358
  const rollback = pinsOwned ? captureConfigTopLevelRollback(config, ["defaultProvider", "modelDiscovery", "disabledModels"]) : undefined;
1372
1359
  let validationError: string | undefined;
1373
1360
  withConfigMutationLockSync(() => {
1361
+ validationError = anthropicInstancePublicationError(name, candidate, config.providers[name]);
1362
+ if (validationError) return;
1374
1363
  const liveTiers = config.providers[name]?.modelContextTiers;
1375
1364
  if (submittedModelContextTiers && liveTiers) candidate.modelContextTiers = Object.assign(Object.create(null), liveTiers, candidate.modelContextTiers ?? {});
1376
1365
  else if (!submittedModelContextTiers && liveTiers) candidate.modelContextTiers = { ...liveTiers };
@@ -1621,7 +1610,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1621
1610
  if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) {
1622
1611
  return jsonResponse({ error: "unknown provider" }, 404);
1623
1612
  }
1624
- const prov = config.providers[name]!;
1613
+ const prov = { ...config.providers[name]! };
1614
+ const authorizedTarget = captureModelsOAuthTarget(name, prov);
1625
1615
  if (prov.disabled) {
1626
1616
  return jsonResponse({ ok: false, error: "Provider is disabled", latencyMs: 0 });
1627
1617
  }
@@ -1646,10 +1636,10 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1646
1636
  // credential resolution/network access for providers such as Antigravity.
1647
1637
  return jsonResponse({ applicable: false, reason: "static_catalog", latencyMs: 0 });
1648
1638
  }
1649
- const { buildModelsRequest, getValidAccessTokenSnapshot, resolveModelsAuthToken } = await import("../../oauth");
1639
+ const { buildModelsRequest, getModelsOAuthAccessSnapshot, resolveModelsAuthToken } = await import("../../oauth");
1650
1640
  const antigravity = effectiveGoogleMode(name, prov) === "cloud-code-assist";
1651
- const snapshot = prov.authMode === "oauth"
1652
- ? await getValidAccessTokenSnapshot(name).catch(() => undefined)
1641
+ const snapshot = prov.authMode === "oauth" && mayResolveModelsOAuth(name, config.providers[name])
1642
+ ? await getModelsOAuthAccessSnapshot(name, prov).catch(() => undefined)
1653
1643
  : undefined;
1654
1644
  const apiKey = prov.authMode === "oauth" ? snapshot?.accessToken : await resolveModelsAuthToken(name, prov);
1655
1645
  if (prov.authMode === "oauth" && !apiKey) {
@@ -1735,7 +1725,10 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1735
1725
  // Same canonical-URL TUN transparency as catalog discovery: the registry's
1736
1726
  // own fixed discovery URL survives purely-benchmark (Clash/Surge/Mihomo
1737
1727
  // fake-IP) DNS without proxy env.
1738
- const outboundDependencies = { isCanonicalUrl: isRegistryModelDiscoveryUrl };
1728
+ const outboundDependencies = { isCanonicalUrl: isRegistryModelDiscoveryUrl,
1729
+ beforeSend: () => prov.authMode !== "oauth" || (mayResolveModelsOAuth(name, config.providers[name])
1730
+ && modelsOAuthTargetMatches(name, config.providers[name], authorizedTarget)
1731
+ && (name !== "anthropic2" || new URL(modelsUrl).href === authorizedTarget)) };
1739
1732
  const res = method === "POST"
1740
1733
  ? await providerOutboundPost(name, prov, modelsUrl, {
1741
1734
  headers,
@@ -231,6 +231,8 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
231
231
  { method: "POST", path: "/api/client-integrations/restore", module: "server/management/integration-routes", mutates: true },
232
232
  { method: "POST", path: "/api/client-integrations/preview", module: "server/management/integration-routes", mutates: false },
233
233
  { method: "POST", path: "/api/client-integrations/restore/preview", module: "server/management/integration-routes", mutates: false },
234
+ { method: "POST", path: "/api/client-integrations/commandcode/restore", module: "server/management/integration-routes", mutates: true },
235
+ { method: "POST", path: "/api/client-integrations/commandcode/restore/preview", module: "server/management/integration-routes", mutates: false },
234
236
  // server/management/lab-automation-routes
235
237
  { method: "GET", path: "/api/lab/automation", module: "server/management/lab-automation-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
236
238
  { method: "GET", path: "/api/lab/automation/runs", module: "server/management/lab-automation-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
@@ -324,7 +326,7 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
324
326
  { method: "PATCH", path: "/api/keys", module: "server/management/oauth-account-routes", mutates: true },
325
327
  { method: "PATCH", path: "/api/oauth/accounts/pool", module: "server/management/oauth-account-routes", mutates: true },
326
328
  { method: "POST", path: "/api/keys", module: "server/management/oauth-account-routes", mutates: true },
327
- { method: "POST", path: "/api/keys/reveal", module: "server/management/oauth-account-routes", mutates: false, exempt: { reason: "session-only", why: "Revealing a data key requires the gui-session principal; the admin token is refused." } },
329
+ { method: "POST", path: "/api/keys/reveal", module: "server/management/oauth-account-routes", mutates: false, exempt: { reason: "session-only", why: "Stored-key reads require a current pairing or trusted Tailscale-identity session; automatic loopback sessions and admin tokens are refused." } },
328
330
  { method: "POST", path: "/api/keys/rotate", module: "server/management/oauth-account-routes", mutates: true },
329
331
  { method: "POST", path: "/api/keys/rotate/commit", module: "server/management/oauth-account-routes", mutates: true },
330
332
  { method: "POST", path: "/api/oauth/accounts/clear-cooldown", module: "server/management/oauth-account-routes", mutates: true },
@@ -63,7 +63,7 @@ function parseEvidence(raw: unknown): { evidence: PolicyRequestEvidence; ok: boo
63
63
  return { evidence, ok: true };
64
64
  }
65
65
 
66
- function parseCandidateEvidence(raw: unknown): PolicyCandidateEvidence[] | null {
66
+ function parseCandidateEvidence(raw: unknown, config: OcxConfig): PolicyCandidateEvidence[] | null {
67
67
  if (!Array.isArray(raw)) return null;
68
68
  const out: PolicyCandidateEvidence[] = [];
69
69
  for (const item of raw) {
@@ -87,10 +87,10 @@ function parseCandidateEvidence(raw: unknown): PolicyCandidateEvidence[] | null
87
87
  // caller does not supply an explicit quota object, so a dry-run following
88
88
  // the documented shape reports the same cached account quota as routing.
89
89
  ...(item.quota === undefined && typeof item.codexAccountId === "string"
90
- ? { quota: quotaEvidenceForCandidate({ provider, model, codexAccountId: item.codexAccountId }) }
90
+ ? { quota: quotaEvidenceForCandidate({ provider, model, codexAccountId: item.codexAccountId }, config) }
91
91
  : {}),
92
92
  ...(item.quota === undefined && typeof item.accountRef === "string" && typeof item.codexAccountId !== "string"
93
- ? { quota: quotaEvidenceForCandidate({ provider, model, accountRef: item.accountRef }) }
93
+ ? { quota: quotaEvidenceForCandidate({ provider, model, accountRef: item.accountRef }, config) }
94
94
  : {}),
95
95
  });
96
96
  }
@@ -392,7 +392,7 @@ export async function handleRoutingProfileRoutes(ctx: ManagementContext): Promis
392
392
  if (labActivationRequired(config, getConfigDir())) activateLab(config, getConfigDir());
393
393
  const candidateEvidence = body.candidates === undefined
394
394
  ? assembleCandidateEvidence(config, resolvedProfile, now)
395
- : parseCandidateEvidence(body.candidates);
395
+ : parseCandidateEvidence(body.candidates, config);
396
396
  if (candidateEvidence === null) {
397
397
  return jsonResponse({ error: { code: "invalid_candidates", message: "candidates must be an array of evidence objects" } }, 400, req, config);
398
398
  }