@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
@@ -14,7 +14,11 @@
14
14
  import { jsonResponse } from "../auth-cors";
15
15
  import type { ManagementContext } from "./context";
16
16
  import { getValidAccessSnapshotForAccount } from "../../oauth";
17
- import { captureOAuthAccountSelection, listAccounts } from "../../oauth/store";
17
+ import { configuredAnthropicInstance, isAnthropicInstanceId, type AnthropicInstanceId } from "../../providers/anthropic-instance";
18
+ import { captureAnthropicPhysicalSendOwnership, anthropicPhysicalSendOwnershipIsCurrent } from "../../oauth/anthropic-send-ownership";
19
+ import { anthropicCooldownRecoveryFor } from "../../providers/quota/anthropic-cooldown-recovery";
20
+ import { captureProviderAccountQuotaEpoch } from "../../providers/quota/account-cache";
21
+ import { captureOAuthAccountSelection, getAccountSet, getAccountCredentialWithStatus, listAccounts } from "../../oauth/store";
18
22
  import {
19
23
  ANTHROPIC_RESET_GRANT_ID_RE,
20
24
  AnthropicResetGrantError,
@@ -28,19 +32,20 @@ import {
28
32
  AnthropicResetLedgerError,
29
33
  anthropicOrgDigest,
30
34
  anthropicResetOperationExists,
35
+ anthropicResetJournalPathForInstance,
31
36
  beginAnthropicResetOperation,
32
37
  pendingAnthropicResetOperation,
33
38
  releaseAnthropicResetLease,
34
39
  settleAnthropicResetOperation,
35
40
  } from "../../providers/anthropic-reset-grant-ledger";
36
41
 
37
- const PROVIDER = "anthropic";
38
42
  const UUID_V4_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
39
43
 
40
44
  const MESSAGES = {
41
45
  method_not_allowed: "Method not allowed",
42
46
  session_required: "Spending a reset requires a dashboard session.",
43
47
  invalid_json: "Request body must be JSON.",
48
+ invalid_provider: "No matching builtin Anthropic OAuth provider.",
44
49
  invalid_grant_id: "grantId is missing or malformed.",
45
50
  invalid_operation_id: "operationId must be a UUIDv4.",
46
51
  no_account: "No matching Anthropic OAuth account.",
@@ -60,6 +65,9 @@ const MESSAGES = {
60
65
  type ErrorCode = keyof typeof MESSAGES;
61
66
 
62
67
  export interface AnthropicResetGrantRouteDeps {
68
+ provider?: AnthropicInstanceId;
69
+ isCurrent?: () => boolean;
70
+ forProvider?: (provider: AnthropicInstanceId) => AnthropicResetGrantRouteDeps;
63
71
  fetchFn?: typeof globalThis.fetch;
64
72
  journalPath?: string;
65
73
  now?: () => number;
@@ -68,25 +76,64 @@ export interface AnthropicResetGrantRouteDeps {
68
76
  accessTokenFor: (accountId: string) => Promise<string>;
69
77
  }
70
78
 
71
- const defaultDeps: AnthropicResetGrantRouteDeps = {
72
- listAccountIds: () => listAccounts(PROVIDER).map(account => account.id),
73
- activeAccountId: () => captureOAuthAccountSelection(PROVIDER)?.accountId ?? null,
74
- accessTokenFor: async accountId =>
75
- (await getValidAccessSnapshotForAccount(PROVIDER, accountId, { requireUsableAccount: true })).accessToken,
76
- };
79
+ function defaultDepsFor(ctx: ManagementContext, provider: AnthropicInstanceId): AnthropicResetGrantRouteDeps | null {
80
+ // One source: the live management config. A keeps its legacy reach without a providers row;
81
+ // B requires its configured builtin row. A fresh file load here would refuse on any drift.
82
+ const live = ctx.config;
83
+ const target = live.providers?.[provider];
84
+ if (target?.disabled || target && target.authMode !== "oauth" || configuredAnthropicInstance(live, provider) !== provider) return null;
85
+ const identity = JSON.stringify(target ?? null);
86
+ let owner: ReturnType<typeof captureAnthropicPhysicalSendOwnership> = null;
87
+ const targetCurrent = () => configuredAnthropicInstance(live, provider) === provider
88
+ && JSON.stringify(live.providers?.[provider] ?? null) === identity;
89
+ return {
90
+ // Only the journal path: the ledger's numeric `now` option is not the deps clock function.
91
+ provider, journalPath: anthropicResetJournalPathForInstance(provider),
92
+ listAccountIds: () => listAccounts(provider).map(account => account.id),
93
+ activeAccountId: () => captureOAuthAccountSelection(provider)?.accountId ?? null,
94
+ isCurrent: () => {
95
+ if (!targetCurrent() || !owner || !anthropicPhysicalSendOwnershipIsCurrent(owner)) return false;
96
+ const row = getAccountCredentialWithStatus(provider, owner.accountId);
97
+ return !!row && !row.paused && !row.needsReauth;
98
+ },
99
+ accessTokenFor: async accountId => {
100
+ const initial = getAccountSet(provider)?.accounts.find(row => row.id === accountId);
101
+ const recovery = anthropicCooldownRecoveryFor(provider);
102
+ const incarnation = recovery.reserveAnthropicAccountIncarnation(accountId);
103
+ const epoch = captureProviderAccountQuotaEpoch(provider);
104
+ const snapshot = await getValidAccessSnapshotForAccount(provider, accountId, { requireUsableAccount: true });
105
+ const current = getAccountSet(provider)?.accounts.find(row => row.id === accountId);
106
+ if (!initial || !current || initial.loginId !== current.loginId || initial.addedAt !== current.addedAt
107
+ || recovery.anthropicAccountIncarnation(accountId) !== incarnation || captureProviderAccountQuotaEpoch(provider) !== epoch) throw new Error("reset account replaced");
108
+ owner = captureAnthropicPhysicalSendOwnership(snapshot);
109
+ if (!targetCurrent() || !owner) throw new Error("reset owner changed");
110
+ return snapshot.accessToken;
111
+ },
112
+ };
113
+ }
114
+
115
+ function selectDeps(ctx: ManagementContext, requested: unknown, deps?: AnthropicResetGrantRouteDeps): AnthropicResetGrantRouteDeps | null {
116
+ const provider = requested === undefined || requested === null ? "anthropic" : requested;
117
+ if (!isAnthropicInstanceId(provider)) return null;
118
+ const selected = deps?.forProvider?.(provider) ?? deps;
119
+ // Legacy injected dependencies are A-only; B must explicitly bind its own journal.
120
+ if (selected) return (selected.provider ?? "anthropic") === provider
121
+ ? { journalPath: anthropicResetJournalPathForInstance(provider), ...selected, provider } : null;
122
+ return defaultDepsFor(ctx, provider);
123
+ }
77
124
 
78
125
  function fail(ctx: ManagementContext, status: number, code: ErrorCode, extra: Record<string, unknown> = {}): Response {
79
126
  return jsonResponse({ error: { code, message: MESSAGES[code], ...extra } }, status, ctx.req, ctx.config);
80
127
  }
81
128
 
82
- function ledgerFailure(ctx: ManagementContext, error: unknown): Response {
83
- if (error instanceof AnthropicResetLedgerError && error.code === "busy") return fail(ctx, 503, "ledger_busy");
84
- return fail(ctx, 503, "ledger_unavailable");
129
+ function ledgerFailure(ctx: ManagementContext, error: unknown, extra: Record<string, unknown> = {}): Response {
130
+ if (error instanceof AnthropicResetLedgerError && error.code === "busy") return fail(ctx, 503, "ledger_busy", extra);
131
+ return fail(ctx, 503, "ledger_unavailable", extra);
85
132
  }
86
133
 
87
- function readFailure(ctx: ManagementContext, error: unknown): Response {
88
- if (error instanceof AnthropicResetGrantError && error.code === "auth") return fail(ctx, 401, "auth_failed");
89
- return fail(ctx, 502, "upstream_unavailable");
134
+ function readFailure(ctx: ManagementContext, error: unknown, extra: Record<string, unknown> = {}): Response {
135
+ if (error instanceof AnthropicResetGrantError && error.code === "auth") return fail(ctx, 401, "auth_failed", extra);
136
+ return fail(ctx, 502, "upstream_unavailable", extra);
90
137
  }
91
138
 
92
139
  function resolveAccountId(deps: AnthropicResetGrantRouteDeps, requested: unknown): string | null {
@@ -107,16 +154,19 @@ async function tokenFor(deps: AnthropicResetGrantRouteDeps, accountId: string):
107
154
  }
108
155
 
109
156
  async function handleRead(ctx: ManagementContext, deps: AnthropicResetGrantRouteDeps): Promise<Response> {
157
+ const receipt = { provider: deps.provider };
110
158
  const accountId = resolveAccountId(deps, ctx.url.searchParams.get("accountId") ?? undefined);
111
- if (!accountId) return fail(ctx, 400, "no_account");
159
+ if (!accountId) return fail(ctx, 400, "no_account", receipt);
112
160
  const accessToken = await tokenFor(deps, accountId);
113
- if (!accessToken) return fail(ctx, 401, "auth_failed");
161
+ if (!accessToken) return fail(ctx, 401, "auth_failed", receipt);
162
+ if (deps.isCurrent?.() === false) return fail(ctx, 401, "auth_failed", receipt);
114
163
  let status;
115
164
  try {
116
165
  status = await fetchAnthropicResetGrantStatus({ accessToken, fetchFn: deps.fetchFn });
117
166
  } catch (error) {
118
- return readFailure(ctx, error);
167
+ return readFailure(ctx, error, receipt);
119
168
  }
169
+ if (deps.isCurrent?.() === false) return fail(ctx, 401, "auth_failed", receipt);
120
170
  let pendingOperation = null;
121
171
  let journalAvailable = true;
122
172
  try {
@@ -124,10 +174,10 @@ async function handleRead(ctx: ManagementContext, deps: AnthropicResetGrantRoute
124
174
  } catch {
125
175
  journalAvailable = false;
126
176
  }
127
- return jsonResponse({ accountId, ...status, pendingOperation, journalAvailable }, 200, ctx.req, ctx.config);
177
+ return jsonResponse({ provider: deps.provider, accountId, ...status, pendingOperation, journalAvailable }, 200, ctx.req, ctx.config);
128
178
  }
129
179
 
130
- async function handleConsume(ctx: ManagementContext, deps: AnthropicResetGrantRouteDeps): Promise<Response> {
180
+ async function handleConsume(ctx: ManagementContext, injected?: AnthropicResetGrantRouteDeps): Promise<Response> {
131
181
  if (ctx.principal !== "gui-session") return fail(ctx, 403, "session_required");
132
182
  let body: Record<string, unknown>;
133
183
  try {
@@ -137,21 +187,27 @@ async function handleConsume(ctx: ManagementContext, deps: AnthropicResetGrantRo
137
187
  } catch {
138
188
  return fail(ctx, 400, "invalid_json");
139
189
  }
190
+ const deps = selectDeps(ctx, body.provider, injected);
191
+ if (!deps) return fail(ctx, 400, "invalid_provider");
192
+ const provider = deps.provider;
193
+ const operationReceipt = { provider };
194
+ const scopedFail = (status: number, code: ErrorCode, extra: Record<string, unknown> = {}) => fail(ctx, status, code, { ...operationReceipt, ...extra });
140
195
  const { grantId, operationId } = body;
141
- if (typeof grantId !== "string" || !ANTHROPIC_RESET_GRANT_ID_RE.test(grantId)) return fail(ctx, 400, "invalid_grant_id");
142
- if (typeof operationId !== "string" || !UUID_V4_RE.test(operationId)) return fail(ctx, 400, "invalid_operation_id");
143
- if (typeof body.accountId !== "string") return fail(ctx, 400, "no_account");
196
+ if (typeof grantId !== "string" || !ANTHROPIC_RESET_GRANT_ID_RE.test(grantId)) return scopedFail(400, "invalid_grant_id");
197
+ if (typeof operationId !== "string" || !UUID_V4_RE.test(operationId)) return scopedFail(400, "invalid_operation_id");
198
+ if (typeof body.accountId !== "string") return scopedFail(400, "no_account");
144
199
  const accountId = resolveAccountId(deps, body.accountId);
145
- if (!accountId) return fail(ctx, 400, "no_account");
200
+ if (!accountId) return scopedFail(400, "no_account");
146
201
 
147
202
  const accessToken = await tokenFor(deps, accountId);
148
- if (!accessToken) return fail(ctx, 401, "auth_failed");
203
+ if (!accessToken || deps.isCurrent?.() === false) return scopedFail(401, "auth_failed");
149
204
  let organizationUuid: string;
150
205
  try {
151
206
  organizationUuid = await fetchAnthropicOrganizationUuid({ accessToken, fetchFn: deps.fetchFn });
152
207
  } catch (error) {
153
- return readFailure(ctx, error);
208
+ return readFailure(ctx, error, operationReceipt);
154
209
  }
210
+ if (deps.isCurrent?.() === false) return scopedFail(401, "auth_failed");
155
211
  const ledger = { journalPath: deps.journalPath, now: deps.now?.() };
156
212
 
157
213
  // A new operation must pass the spend gate on a fresh read. A same-id retry of
@@ -161,39 +217,40 @@ async function handleConsume(ctx: ManagementContext, deps: AnthropicResetGrantRo
161
217
  try {
162
218
  known = anthropicResetOperationExists(operationId, ledger);
163
219
  } catch (error) {
164
- return ledgerFailure(ctx, error);
220
+ return ledgerFailure(ctx, error, operationReceipt);
165
221
  }
166
222
  if (!known) {
167
223
  let status;
168
224
  try {
169
225
  status = await fetchAnthropicResetGrantStatus({ accessToken, fetchFn: deps.fetchFn });
170
226
  } catch (error) {
171
- return readFailure(ctx, error);
227
+ return readFailure(ctx, error, operationReceipt);
172
228
  }
173
229
  const blocker = anthropicResetGrantBlocker(status, grantId);
174
- if (blocker) return fail(ctx, 409, "grant_not_usable", { reason: blocker });
230
+ if (blocker) return scopedFail(409, "grant_not_usable", { reason: blocker });
175
231
  }
176
232
 
177
233
  let begin;
234
+ if (deps.isCurrent?.() === false) return scopedFail(401, "auth_failed");
178
235
  try {
179
236
  begin = beginAnthropicResetOperation(
180
237
  { operationId, accountId, grantId, orgDigest: anthropicOrgDigest(organizationUuid) },
181
238
  { journalPath: deps.journalPath, now: deps.now?.() },
182
239
  );
183
240
  } catch (error) {
184
- return ledgerFailure(ctx, error);
241
+ return ledgerFailure(ctx, error, operationReceipt);
185
242
  }
186
243
  switch (begin.kind) {
187
244
  case "replay":
188
245
  return jsonResponse(
189
- { code: begin.code, replayed: true, resetsLeft: begin.resetsLeft, accountId, grantId, operationId },
246
+ { provider, code: begin.code, replayed: true, resetsLeft: begin.resetsLeft, accountId, grantId, operationId },
190
247
  200, ctx.req, ctx.config,
191
248
  );
192
- case "identity-mismatch": return fail(ctx, 409, "operation_identity_mismatch");
193
- case "in-flight": return fail(ctx, 409, "in_flight");
194
- case "expired": return fail(ctx, 409, "unknown_outcome_expired");
195
- case "unresolved-prior": return fail(ctx, 409, "unresolved_prior_operation", { pendingOperationId: begin.operationId });
196
- case "capacity": return fail(ctx, 503, "ledger_capacity");
249
+ case "identity-mismatch": return scopedFail(409, "operation_identity_mismatch");
250
+ case "in-flight": return scopedFail(409, "in_flight");
251
+ case "expired": return scopedFail(409, "unknown_outcome_expired");
252
+ case "unresolved-prior": return scopedFail(409, "unresolved_prior_operation", { pendingOperationId: begin.operationId });
253
+ case "capacity": return scopedFail(503, "ledger_capacity");
197
254
  case "execute": break;
198
255
  }
199
256
 
@@ -206,7 +263,7 @@ async function handleConsume(ctx: ManagementContext, deps: AnthropicResetGrantRo
206
263
  try {
207
264
  releaseAnthropicResetLease(operationId, { journalPath: deps.journalPath, now: deps.now?.() });
208
265
  } catch { /* the lease expires on its own */ }
209
- return fail(ctx, 502, "unknown_outcome", { operationId, grantId });
266
+ return scopedFail(502, "unknown_outcome", { operationId, grantId, accountId });
210
267
  }
211
268
  let settled;
212
269
  try {
@@ -216,13 +273,14 @@ async function handleConsume(ctx: ManagementContext, deps: AnthropicResetGrantRo
216
273
  );
217
274
  } catch {
218
275
  // Fail closed: the caller must not read this as a durable result.
219
- return fail(ctx, 500, "journal_write_failed", { operationId, grantId });
276
+ return scopedFail(500, "journal_write_failed", { operationId, grantId, accountId });
220
277
  }
221
278
  // Report what the journal holds. An overlapping same-id attempt may have
222
279
  // settled first; its answer is the canonical one.
223
280
  const replayed = settled.code !== answer.code || settled.resetsLeft !== answer.resetsLeft;
224
281
  return jsonResponse(
225
282
  {
283
+ provider,
226
284
  code: settled.code,
227
285
  replayed,
228
286
  resetsLeft: settled.resetsLeft,
@@ -237,12 +295,13 @@ async function handleConsume(ctx: ManagementContext, deps: AnthropicResetGrantRo
237
295
 
238
296
  export async function handleAnthropicResetGrantRoutes(
239
297
  ctx: ManagementContext,
240
- deps: AnthropicResetGrantRouteDeps = defaultDeps,
298
+ deps?: AnthropicResetGrantRouteDeps,
241
299
  ): Promise<Response | null> {
242
300
  const { pathname } = ctx.url;
243
301
  if (pathname === "/api/anthropic/reset-grants") {
244
302
  if (ctx.req.method !== "GET") return fail(ctx, 405, "method_not_allowed");
245
- return handleRead(ctx, deps);
303
+ const selected = selectDeps(ctx, ctx.url.searchParams.get("provider"), deps);
304
+ return selected ? handleRead(ctx, selected) : fail(ctx, 400, "invalid_provider");
246
305
  }
247
306
  if (pathname === "/api/anthropic/reset-grants/consume") {
248
307
  if (ctx.req.method !== "POST") return fail(ctx, 405, "method_not_allowed");
@@ -4,6 +4,8 @@ import { getMainAccountExternalUsageWarning } from "../../codex/main-account-ext
4
4
  import { getObservedMainQuotaIdentityKey } from "../../codex/main-account-cache";
5
5
  import { compactionRoutingSchema, memoryModelsSchema } from "../../config/schema/leaf-validators";
6
6
  import { compactionRecoverySchema } from "../../config/schema/compaction-recovery";
7
+ import { anthropicSidecarPatchError } from "../../config/schema/anthropic-account-pool";
8
+ import { isAnthropicInstanceId } from "../../providers/anthropic-instance-id";
7
9
  import { captureConfigTopLevelRollback } from "../../config/rebase-provenance";
8
10
  import type { IntegrationClientId } from "../../integrations/registry";
9
11
  import { randomUUID } from "node:crypto";
@@ -85,7 +87,6 @@ import { getProviderRegistryEntry } from "../../providers/registry";
85
87
  import { VISION_REASONING_EFFORTS, isVisionReasoningEffort } from "../../reasoning-effort";
86
88
  import { normalizeVisionReasoningForModel } from "../../vision/reasoning";
87
89
  import {
88
- findAnthropicVisionProvider,
89
90
  isValidVisionTimeoutMs,
90
91
  MAX_VISION_TIMEOUT_MS,
91
92
  MIN_VISION_TIMEOUT_MS,
@@ -105,6 +106,7 @@ import {
105
106
  webSearchModelIsRejected,
106
107
  webSearchModelOptionsFrom,
107
108
  webSearchModelRejection,
109
+ sidecarAnthropicPoolOptions, sidecarOptionsAuth, sidecarSettingsAfterPatch,
108
110
  } from "./web-search-sidecar-options";
109
111
  import { validateXaiSearchOptions } from "../../web-search/xai-executor";
110
112
  import { getDebugLogEntries } from "../../lib/debug-log-buffer";
@@ -150,7 +152,9 @@ async function sidecarVisionResponseSettings(config: OcxConfig): Promise<{
150
152
  const vs = config.visionSidecar ?? {};
151
153
  // Match the runtime's one selected Anthropic executor for both backend fallback
152
154
  // and catalog reachability; resolving it once prevents the two projections drifting.
153
- const anthropicSidecar = findAnthropicVisionProvider(config);
155
+ const auth = sidecarOptionsAuth(config, vs.anthropicInstance);
156
+ const anthropicSidecar = auth.isAnthropicAuth && auth.anthropicProviderName && auth.anthropicProvider
157
+ ? { providerName: auth.anthropicProviderName, provider: auth.anthropicProvider, config } : undefined;
154
158
  // The routed backend reports its own namespaced model verbatim: it is the
155
159
  // dispatched value, and collapsing it through the legacy resolver would
156
160
  // display a describer the runtime is not using (roadmap 190).
@@ -276,7 +280,7 @@ export async function syncEnabledClientIntegrations(
276
280
  },
277
281
  config,
278
282
  port,
279
- }, ["mcode", "pi", "aside", "raycast", "omo", "cline", "droid", "opencode", "kilo"]));
283
+ }, ["mcode", "pi", "aside", "raycast", "omo", "cline", "commandcode", "droid", "opencode", "kilo"]));
280
284
 
281
285
  return out;
282
286
  }
@@ -290,6 +294,8 @@ function publicVisionSidecarSettings(
290
294
  enabled: vs.enabled !== false,
291
295
  model: vision.model,
292
296
  backend: vs.backend,
297
+ ...(vs.anthropicInstance ? { anthropicInstance: vs.anthropicInstance } : {}),
298
+ anthropicPool: sidecarAnthropicPoolOptions(config, { ...vs, backend: vs.backend ?? (vision.models.find(row => row.value === vision.model)?.backend) }),
293
299
  reasoning: vision.reasoning,
294
300
  maxDescriptionsPerTurn: resolveMaxDescriptionsPerTurn(vs.maxDescriptionsPerTurn),
295
301
  timeoutMs: resolveVisionTimeoutMs(vs.timeoutMs),
@@ -882,6 +888,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
882
888
  enabled: ws.enabled !== false,
883
889
  model: ws.model ?? "gpt-5.6-luna",
884
890
  backend: ws.backend,
891
+ ...(ws.anthropicInstance ? { anthropicInstance: ws.anthropicInstance } : {}),
892
+ anthropicPool: sidecarAnthropicPoolOptions(config, ws),
885
893
  streamRoutedModelOutput: ws.streamRoutedModelOutput === true,
886
894
  ...(ws.xSearch ? { xSearch: ws.xSearch } : {}),
887
895
  },
@@ -902,10 +910,11 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
902
910
  if (raw.webSearch !== undefined && !isPlainRecord(raw.webSearch)) return jsonResponse({ error: "webSearch must be an object" }, 400);
903
911
  if (raw.vision !== undefined && !isPlainRecord(raw.vision)) return jsonResponse({ error: "vision must be an object" }, 400);
904
912
  const body = raw as {
905
- webSearch?: { enabled?: unknown; model?: unknown; backend?: unknown; reasoning?: unknown; streamRoutedModelOutput?: unknown; exaApiKey?: unknown; xSearch?: unknown };
913
+ webSearch?: { enabled?: unknown; model?: unknown; backend?: unknown; anthropicInstance?: unknown; reasoning?: unknown; streamRoutedModelOutput?: unknown; exaApiKey?: unknown; xSearch?: unknown };
906
914
  vision?: {
907
915
  model?: unknown;
908
916
  backend?: unknown;
917
+ anthropicInstance?: unknown;
909
918
  reasoning?: unknown;
910
919
  maxDescriptionsPerTurn?: unknown;
911
920
  enabled?: unknown;
@@ -913,6 +922,11 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
913
922
  };
914
923
  };
915
924
  const WEB_SEARCH_BACKENDS_UNION = ["openai", "anthropic", "xai", "gemini", "exa"] as const;
925
+ const instanceError = anthropicSidecarPatchError(config, {
926
+ ...(body.webSearch ? { webSearchSidecar: body.webSearch } : {}),
927
+ ...(body.vision ? { visionSidecar: body.vision } : {}),
928
+ });
929
+ if (instanceError) return jsonResponse({ error: instanceError }, 400);
916
930
  if (body.webSearch && body.webSearch.backend !== undefined && body.webSearch.backend !== null
917
931
  && !WEB_SEARCH_BACKENDS_UNION.includes(body.webSearch.backend as never)) {
918
932
  return jsonResponse({ error: "webSearch.backend must be openai, anthropic, xai, gemini, exa, or null" }, 400);
@@ -956,7 +970,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
956
970
  // rejection body, so a 400 cannot cost two provider fetches.
957
971
  if (body.vision && typeof body.vision.model === "string" && body.vision.model !== "") {
958
972
  const requested = body.vision.model;
959
- const candidates = await visionCandidateRows(config);
973
+ const candidates = await visionCandidateRows({ ...config, visionSidecar: sidecarSettingsAfterPatch(config.visionSidecar, body.vision) });
960
974
  const hint = body.vision.backend === "anthropic" || body.vision.backend === "openai"
961
975
  || body.vision.backend === "routed"
962
976
  ? body.vision.backend
@@ -994,7 +1008,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
994
1008
  // only case that owes a config.toml rewrite.
995
1009
  const webSearchEnabledBefore = config.webSearchSidecar?.enabled !== false;
996
1010
  if (body.webSearch) {
997
- const pairTouched = body.webSearch.model !== undefined || body.webSearch.backend !== undefined;
1011
+ const pairTouched = body.webSearch.model !== undefined || body.webSearch.backend !== undefined || body.webSearch.anthropicInstance !== undefined;
998
1012
  // Validate against the backend the caller SUBMITTED, across the whole
999
1013
  // union — not just openai/anthropic (#2457). The union check above has
1000
1014
  // already refused unknown literals, so a surviving string is a member;
@@ -1013,12 +1027,14 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
1013
1027
  ? body.webSearch.model || undefined
1014
1028
  : config.webSearchSidecar?.model;
1015
1029
  if (pairTouched && effectiveModel) {
1016
- const candidates = await webSearchCandidateRows(config);
1030
+ const candidates = await webSearchCandidateRows({ ...config, webSearchSidecar: sidecarSettingsAfterPatch(config.webSearchSidecar, body.webSearch) });
1017
1031
  if (webSearchModelIsRejected(effectiveBackend, effectiveModel, candidates)) {
1018
1032
  return jsonResponse(webSearchModelRejection("webSearch.model", effectiveBackend, effectiveModel, candidates), 400);
1019
1033
  }
1020
1034
  }
1021
1035
  const webSearchCandidate = { ...config.webSearchSidecar };
1036
+ if (body.webSearch.anthropicInstance === null) delete webSearchCandidate.anthropicInstance;
1037
+ else if (isAnthropicInstanceId(body.webSearch.anthropicInstance)) webSearchCandidate.anthropicInstance = body.webSearch.anthropicInstance;
1022
1038
  if (typeof body.webSearch.model === "string") {
1023
1039
  if (body.webSearch.model === "") delete webSearchCandidate.model;
1024
1040
  else webSearchCandidate.model = body.webSearch.model;
@@ -1098,6 +1114,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
1098
1114
  }
1099
1115
  if (body.vision) {
1100
1116
  config.visionSidecar = { ...config.visionSidecar };
1117
+ if (body.vision.anthropicInstance === null) delete config.visionSidecar.anthropicInstance;
1118
+ else if (isAnthropicInstanceId(body.vision.anthropicInstance)) config.visionSidecar.anthropicInstance = body.vision.anthropicInstance;
1101
1119
  if (typeof body.vision.model === "string") {
1102
1120
  if (body.vision.model === "") delete config.visionSidecar.model;
1103
1121
  else config.visionSidecar.model = body.vision.model;
@@ -1143,6 +1161,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
1143
1161
  enabled: ws.enabled !== false,
1144
1162
  model: ws.model ?? "gpt-5.6-luna",
1145
1163
  backend: ws.backend,
1164
+ ...(ws.anthropicInstance ? { anthropicInstance: ws.anthropicInstance } : {}),
1165
+ anthropicPool: sidecarAnthropicPoolOptions(config, ws),
1146
1166
  streamRoutedModelOutput: ws.streamRoutedModelOutput === true,
1147
1167
  ...(ws.xSearch ? { xSearch: ws.xSearch } : {}),
1148
1168
  },
@@ -1,4 +1,5 @@
1
1
  import { jsonResponse } from "../auth-cors";
2
+ import { jevDecisionEndpointUrl } from "../../combos/jev-decision-contract";
2
3
  import type { ManagementContext } from "./context";
3
4
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
4
5
  import { isPlainRecord } from "./shared";
@@ -29,7 +30,7 @@ function decisionServiceIssue(
29
30
  provider: DecisionRow,
30
31
  isSystemOneEndpoint: (url: string) => boolean,
31
32
  ): { url: string; model: string; issue?: "disabled" | "endpoint" | "model" } {
32
- const url = typeof provider.baseUrl === "string" ? provider.baseUrl.trim().replace(/\/+$/, "") : "";
33
+ const url = typeof provider.baseUrl === "string" ? jevDecisionEndpointUrl(provider.baseUrl) : "";
33
34
  const model = provider.defaultModel?.trim() || provider.models?.[0]?.trim() || "";
34
35
  const issue = provider.disabled === true
35
36
  ? "disabled"
@@ -318,6 +318,25 @@ function invalidClientResponse(ctx: ManagementContext): Response {
318
318
  }, 400, ctx.req, ctx.config);
319
319
  }
320
320
 
321
+ /** Bind an explicitly scoped restore before profile delegation or mutation. */
322
+ function restoreClientScopeResponse(ctx: ManagementContext, opId: string, expectedClientId: unknown, pathClientId?: IntegrationClientId): Response | null {
323
+ if (pathClientId !== undefined) {
324
+ if (expectedClientId !== undefined && (typeof expectedClientId !== "string" || !isIntegrationClientId(expectedClientId))) return invalidClientResponse(ctx);
325
+ if (expectedClientId !== undefined && expectedClientId !== pathClientId) return jsonResponse({ error: "restore identity does not match the client path", code: "integration_client_mismatch" }, 409, ctx.req, ctx.config);
326
+ expectedClientId = pathClientId;
327
+ }
328
+ if (expectedClientId === undefined) return null;
329
+ if (typeof expectedClientId !== "string" || !isIntegrationClientId(expectedClientId)) return invalidClientResponse(ctx);
330
+ try {
331
+ const operation = integrationStore().findOperation(opId);
332
+ if (!operation) return jsonResponse({ error: "integration operation not found", code: "integration_operation_not_found", opId }, 404, ctx.req, ctx.config);
333
+ if (operation.clientId !== expectedClientId) return jsonResponse({ error: "integration operation belongs to a different client", code: "integration_client_mismatch" }, 409, ctx.req, ctx.config);
334
+ return null;
335
+ } catch (error) {
336
+ return internalErrorResponse(error, ctx);
337
+ }
338
+ }
339
+
321
340
  function isStringRecord(value: unknown): value is Record<string, string> {
322
341
  return value !== null && typeof value === "object" && !Array.isArray(value)
323
342
  && Object.values(value).every(item => typeof item === "string");
@@ -775,7 +794,7 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
775
794
  }
776
795
  }
777
796
 
778
- if (url.pathname === "/api/client-integrations/restore/preview") {
797
+ if (url.pathname === "/api/client-integrations/restore/preview" || url.pathname === "/api/client-integrations/commandcode/restore/preview") {
779
798
  if (req.method !== "POST") return null;
780
799
  const parsed = await readJsonBody(ctx);
781
800
  if (parsed instanceof Response) return parsed;
@@ -786,6 +805,8 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
786
805
  return jsonResponse({ error: "confirmDrift must be a boolean", code: "invalid_confirm_drift" }, 400, req, ctx.config);
787
806
  }
788
807
  const opId = parsed.opId.trim();
808
+ const scopeResponse = restoreClientScopeResponse(ctx, opId, parsed.expectedClientId, url.pathname === "/api/client-integrations/commandcode/restore/preview" ? "commandcode" : undefined);
809
+ if (scopeResponse) return scopeResponse;
789
810
  try {
790
811
  const store = integrationStore();
791
812
  const operation = store.findOperation(opId);
@@ -819,7 +840,7 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
819
840
  }
820
841
  }
821
842
 
822
- if (url.pathname === "/api/client-integrations/restore") {
843
+ if (url.pathname === "/api/client-integrations/restore" || url.pathname === "/api/client-integrations/commandcode/restore") {
823
844
  if (req.method !== "POST") return null;
824
845
  const parsed = await readJsonBody(ctx);
825
846
  if (parsed instanceof Response) return parsed;
@@ -837,6 +858,8 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
837
858
  }
838
859
 
839
860
  const opId = parsed.opId.trim();
861
+ const scopeResponse = restoreClientScopeResponse(ctx, opId, parsed.expectedClientId, url.pathname === "/api/client-integrations/commandcode/restore" ? "commandcode" : undefined);
862
+ if (scopeResponse) return scopeResponse;
840
863
  const confirmDrift = parsed.confirmDrift ?? false;
841
864
  const restoreBinding = planBindingOf(parsed);
842
865
  if (restoreBinding === "half") return halfBoundResponse(ctx);