@bitkyc08/opencodex 2.80.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 (227) 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/devin/live-models.ts +2 -0
  20. package/src/adapters/kiro-retry.ts +9 -4
  21. package/src/adapters/openai-chat/messages.ts +7 -6
  22. package/src/adapters/physical-send.ts +8 -5
  23. package/src/adapters/qoder/adapter.ts +41 -6
  24. package/src/adapters/qoder/scaffold-guard.ts +2 -3
  25. package/src/adapters/zed.ts +16 -1
  26. package/src/claude/inbound-cache-stabilize.ts +4 -9
  27. package/src/claude/inbound.ts +1 -0
  28. package/src/claude/intercept/model-bindings.ts +1 -0
  29. package/src/claude/intercept/picker-listener.ts +71 -4
  30. package/src/cli/account-anthropic-threshold.ts +14 -5
  31. package/src/cli/account-api.ts +3 -1
  32. package/src/cli/account-extended.ts +13 -9
  33. package/src/cli/account-policy-dto.ts +1 -1
  34. package/src/cli/account-policy.ts +13 -4
  35. package/src/cli/account.ts +3 -3
  36. package/src/cli/capabilities-accounts.ts +2 -2
  37. package/src/cli/capabilities-agents-routing.ts +24 -0
  38. package/src/cli/capabilities-integrations.ts +16 -0
  39. package/src/cli/codex-shim-autorestore.ts +3 -0
  40. package/src/cli/dispatch.ts +10 -1
  41. package/src/cli/help.ts +4 -1
  42. package/src/cli/index.ts +11 -5
  43. package/src/cli/integration-plan-dto.ts +1 -0
  44. package/src/cli/integration-preview.ts +8 -1
  45. package/src/cli/integrations.ts +32 -3
  46. package/src/cli/message-args.ts +34 -0
  47. package/src/cli/message-command.ts +43 -0
  48. package/src/cli/message-runtime.ts +6 -0
  49. package/src/cli/observe.ts +3 -1
  50. package/src/cli/provider.ts +29 -6
  51. package/src/cli/registry.ts +24 -2
  52. package/src/clients/config-export/commandcode.ts +131 -0
  53. package/src/clients/config-export/contracts.ts +12 -0
  54. package/src/clients/config-export.ts +64 -1
  55. package/src/codex/auth-api/account-list.ts +11 -3
  56. package/src/codex/auth-api/main-account-probe.ts +25 -8
  57. package/src/codex/auth-api/pool-mode-gate.ts +33 -9
  58. package/src/codex/auth-api/pool-quota-probe.ts +15 -1
  59. package/src/codex/auth-api/routes.ts +1 -0
  60. package/src/codex/catalog/auto-review.ts +13 -6
  61. package/src/codex/catalog/build-entries.ts +15 -7
  62. package/src/codex/catalog/control-plane.ts +45 -0
  63. package/src/codex/catalog/effort.ts +11 -1
  64. package/src/codex/catalog/gather-capture.ts +5 -3
  65. package/src/codex/catalog/parsing.ts +2 -1
  66. package/src/codex/catalog/provider-models.ts +15 -9
  67. package/src/codex/control-plane-models.ts +6 -0
  68. package/src/combos/jev-decision-contract.ts +25 -2
  69. package/src/combos/jev.ts +3 -2
  70. package/src/combos/types.ts +1 -1
  71. package/src/config/diagnostics.ts +27 -16
  72. package/src/config/load-degrade.ts +34 -0
  73. package/src/config/schema/anthropic-account-pool.ts +143 -0
  74. package/src/config/schema/config-schema.ts +7 -1
  75. package/src/config/schema/leaf-validators.ts +4 -0
  76. package/src/config.ts +2 -0
  77. package/src/generated/compatibility-version.json +320 -184
  78. package/src/generated/model-metadata.ts +5 -4
  79. package/src/images/loop.ts +2 -1
  80. package/src/integrations/catalog-refresh.ts +1 -1
  81. package/src/integrations/mutation-plan.ts +7 -0
  82. package/src/integrations/registry.ts +8 -0
  83. package/src/integrations/state.ts +9 -2
  84. package/src/integrations/writer.ts +8 -2
  85. package/src/lib/lab-live-route-production.ts +9 -1
  86. package/src/lib/request-execution-budget.ts +236 -58
  87. package/src/lib/spend-pool-alias-validation.ts +79 -0
  88. package/src/lib/spend-pool-continuity.ts +34 -0
  89. package/src/lib/spend-reservation-ledger.ts +412 -50
  90. package/src/lib/state-store-registrations.ts +20 -3
  91. package/src/lib/upstream-retry.ts +17 -5
  92. package/src/lib/workflow-budget.ts +53 -11
  93. package/src/messaging/budget.ts +45 -0
  94. package/src/messaging/discovery.ts +57 -0
  95. package/src/messaging/envelope.ts +49 -0
  96. package/src/messaging/input.ts +37 -0
  97. package/src/messaging/rpc.ts +196 -0
  98. package/src/messaging/send.ts +58 -0
  99. package/src/messaging/socket.ts +72 -0
  100. package/src/messaging/types.ts +29 -0
  101. package/src/oauth/anthropic-account-refusal.ts +51 -17
  102. package/src/oauth/anthropic-account-threshold.ts +8 -2
  103. package/src/oauth/anthropic-continuity.ts +15 -1
  104. package/src/oauth/anthropic-model-quota.ts +120 -48
  105. package/src/oauth/anthropic-model-routes.ts +11 -3
  106. package/src/oauth/anthropic-oauth-definitions.ts +98 -0
  107. package/src/oauth/anthropic-pool-config.ts +30 -0
  108. package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
  109. package/src/oauth/anthropic-routing.ts +1202 -866
  110. package/src/oauth/anthropic-send-ownership.ts +41 -0
  111. package/src/oauth/anthropic.ts +4 -1
  112. package/src/oauth/generic-account-failover.ts +3 -1
  113. package/src/oauth/health.ts +8 -6
  114. package/src/oauth/index.ts +90 -173
  115. package/src/oauth/kiro.ts +22 -9
  116. package/src/oauth/login-cli.ts +2 -0
  117. package/src/oauth/model-discovery-auth.ts +43 -0
  118. package/src/oauth/pool-kernel.ts +11 -5
  119. package/src/oauth/pool-settings-capability.ts +5 -3
  120. package/src/oauth/store-anthropic-instance.ts +87 -0
  121. package/src/oauth/store.ts +67 -9
  122. package/src/oauth/token-guardian.ts +2 -0
  123. package/src/oauth/types.ts +2 -0
  124. package/src/protocols/plan-snapshot.ts +5 -0
  125. package/src/protocols/settings.ts +20 -5
  126. package/src/providers/anthropic-instance-id.ts +47 -0
  127. package/src/providers/anthropic-instance.ts +48 -0
  128. package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
  129. package/src/providers/derive.ts +2 -0
  130. package/src/providers/kiro-models.ts +3 -0
  131. package/src/providers/label.ts +1 -1
  132. package/src/providers/model-rename-fields.ts +2 -0
  133. package/src/providers/quota/account-cache.ts +37 -11
  134. package/src/providers/quota/anthropic-account-quota.ts +93 -0
  135. package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
  136. package/src/providers/quota/report-cache.ts +24 -1
  137. package/src/providers/quota/vendor-probes-oauth.ts +46 -14
  138. package/src/providers/quota.ts +22 -32
  139. package/src/providers/registry/entries-core.ts +35 -30
  140. package/src/providers/registry/model-ids.ts +1 -0
  141. package/src/providers/registry/model-seeds.ts +7 -4
  142. package/src/providers/registry/types.ts +3 -0
  143. package/src/providers/registry.ts +3 -3
  144. package/src/responses/spill-store.ts +5 -1
  145. package/src/responses/state/metrics.ts +87 -0
  146. package/src/responses/state/snapshot-codec.ts +6 -0
  147. package/src/responses/state/spill-queue.ts +1 -0
  148. package/src/responses/state/unforced-store-false.ts +55 -0
  149. package/src/responses/state.ts +50 -66
  150. package/src/router.ts +28 -4
  151. package/src/routing/compatibility/assemble.ts +1 -1
  152. package/src/routing/quota.ts +7 -3
  153. package/src/server/auth-cors.ts +5 -0
  154. package/src/server/chat-completions.ts +6 -1
  155. package/src/server/chat-native.ts +45 -25
  156. package/src/server/claude-messages.ts +16 -2
  157. package/src/server/gui-static.ts +38 -4
  158. package/src/server/index/spend-ledger-lifecycle.ts +10 -4
  159. package/src/server/index.ts +4 -2
  160. package/src/server/inference/context.ts +1 -1
  161. package/src/server/lifecycle.ts +1 -1
  162. package/src/server/management/agent-settings-routes.ts +30 -6
  163. package/src/server/management/anthropic-account-threshold.ts +12 -9
  164. package/src/server/management/anthropic-pool-settings.ts +72 -0
  165. package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
  166. package/src/server/management/config-routes.ts +27 -7
  167. package/src/server/management/decision-routes.ts +2 -1
  168. package/src/server/management/integration-routes.ts +25 -2
  169. package/src/server/management/oauth-account-routes.ts +79 -98
  170. package/src/server/management/provider-instance-ownership.ts +44 -0
  171. package/src/server/management/provider-routes.ts +21 -28
  172. package/src/server/management/route-registry.ts +3 -1
  173. package/src/server/management/routing-profile-routes.ts +4 -4
  174. package/src/server/management/system-restart.ts +44 -14
  175. package/src/server/management/system-routes.ts +24 -2
  176. package/src/server/management/vision-sidecar-options.ts +9 -6
  177. package/src/server/management/web-search-sidecar-options.ts +51 -2
  178. package/src/server/management-auth.ts +6 -0
  179. package/src/server/messages-native-eligibility.ts +3 -2
  180. package/src/server/messages-native-oauth.ts +86 -41
  181. package/src/server/messages-native-selector.ts +39 -0
  182. package/src/server/messages-native.ts +84 -25
  183. package/src/server/request-log.ts +5 -0
  184. package/src/server/responses/adapter-continuation.ts +29 -17
  185. package/src/server/responses/adapter-dispatch.ts +47 -17
  186. package/src/server/responses/antigravity-validation-refusal.ts +20 -6
  187. package/src/server/responses/compact.ts +25 -8
  188. package/src/server/responses/compaction-recovery.ts +1 -1
  189. package/src/server/responses/core-combo-native.ts +4 -1
  190. package/src/server/responses/core-combo.ts +16 -11
  191. package/src/server/responses/core-normalize.ts +1 -0
  192. package/src/server/responses/core-options.ts +3 -1
  193. package/src/server/responses/core-replay.ts +5 -0
  194. package/src/server/responses/passthrough-delivery.ts +7 -3
  195. package/src/server/responses/passthrough-dispatch.ts +21 -8
  196. package/src/server/responses/request-prepare.ts +37 -5
  197. package/src/server/responses/request-send-budget.ts +74 -37
  198. package/src/server/responses/request-spend.ts +325 -33
  199. package/src/server/responses/request-transport.ts +132 -52
  200. package/src/server/responses/run-turn-execution.ts +11 -3
  201. package/src/server/responses/sidecar-execution.ts +96 -14
  202. package/src/server/responses-undeclared-tool-guard.ts +5 -1
  203. package/src/server/workflow-refusal.ts +30 -0
  204. package/src/service/ownership-mutation-lease.d.mts +1 -1
  205. package/src/service/ownership-mutation-lease.mjs +5 -7
  206. package/src/sidecar/anthropic-binding.ts +79 -0
  207. package/src/sidecar/auth.ts +76 -2
  208. package/src/types/anthropic-account-pool.ts +33 -0
  209. package/src/types/config.ts +12 -28
  210. package/src/types/provider.ts +5 -0
  211. package/src/types/tools.ts +7 -0
  212. package/src/types.ts +4 -0
  213. package/src/usage/cost.ts +3 -2
  214. package/src/usage/expected-prices.ts +66 -14
  215. package/src/vision/anthropic-describe.ts +16 -3
  216. package/src/vision/index.ts +3 -0
  217. package/src/vision/plan.ts +19 -6
  218. package/src/web-search/alpha-search.ts +7 -1
  219. package/src/web-search/anthropic-executor.ts +16 -4
  220. package/src/web-search/index.ts +7 -3
  221. package/src/web-search/loop.ts +27 -5
  222. package/src/web-search/passthrough-bridge.ts +4 -1
  223. package/src/web-search/sidecar-providers.ts +4 -3
  224. package/gui/dist/assets/App-MbDVfUeC.js +0 -54
  225. package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
  226. package/gui/dist/assets/index-UJt6npXk.js +0 -94
  227. package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
@@ -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
  }
@@ -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
  }