@bitkyc08/opencodex 2.63.0 → 2.64.0-preview.20260923

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 (262) hide show
  1. package/AGENTS_INSTALL.md +4 -3
  2. package/README.md +46 -33
  3. package/assets/download-linux.svg +10 -0
  4. package/assets/download-macos.svg +10 -0
  5. package/assets/download-windows.svg +10 -0
  6. package/bin/ocx.mjs +12 -4
  7. package/gui/dist/assets/App-D5eN1TID.js +50 -0
  8. package/gui/dist/assets/App-I5AnaSLh.css +1 -0
  9. package/gui/dist/assets/{Tray-CncKDBTp.js → Tray-Bh_ErQDh.js} +1 -1
  10. package/gui/dist/assets/index-BEq4OCOz.js +86 -0
  11. package/gui/dist/assets/index-DiBRuK-d.css +1 -0
  12. package/gui/dist/assets/usage-companion-chart-DBG_37kQ.js +1 -0
  13. package/gui/dist/index.html +2 -2
  14. package/native/remote-workspace-helper/src/protocol.rs +5 -0
  15. package/package.json +4 -1
  16. package/src/adapters/anthropic.ts +66 -7
  17. package/src/adapters/codebuddy/adapter.ts +112 -16
  18. package/src/adapters/codebuddy/mcp-server.ts +180 -0
  19. package/src/adapters/codebuddy/scaffold-guard.ts +25 -8
  20. package/src/adapters/codebuddy/tool-bridge.ts +597 -0
  21. package/src/adapters/coding-agent/protocol.ts +123 -10
  22. package/src/adapters/coding-agent/turn.ts +324 -2
  23. package/src/adapters/command-code-restored-schema.ts +112 -0
  24. package/src/adapters/command-code-tool-text.ts +589 -0
  25. package/src/adapters/command-code.ts +80 -6
  26. package/src/adapters/cursor/catalog.ts +12 -0
  27. package/src/adapters/cursor/current-request.ts +46 -0
  28. package/src/adapters/cursor/discovery.ts +1 -1
  29. package/src/adapters/cursor/effort-map.ts +4 -0
  30. package/src/adapters/cursor/native-exec.ts +15 -0
  31. package/src/adapters/cursor/protobuf-events.ts +40 -12
  32. package/src/adapters/cursor/protobuf-request.ts +83 -18
  33. package/src/adapters/cursor/request-builder.ts +5 -4
  34. package/src/adapters/cursor/tool-guidance.ts +1 -1
  35. package/src/adapters/devin/live-models.ts +7 -0
  36. package/src/adapters/inline-think-tags.ts +251 -0
  37. package/src/adapters/kiro/adapter.ts +1 -0
  38. package/src/adapters/kiro/stream.ts +5 -3
  39. package/src/adapters/kiro/usage.ts +4 -3
  40. package/src/adapters/mimo-free.ts +37 -18
  41. package/src/adapters/openai-chat/messages.ts +5 -5
  42. package/src/adapters/openai-chat/tool-schema.ts +124 -2
  43. package/src/adapters/openai-chat.ts +13 -3
  44. package/src/adapters/openai-responses/passthrough.ts +15 -2
  45. package/src/adapters/openai-responses/reasoning.ts +14 -1
  46. package/src/adapters/openai-responses/request-strips.ts +29 -3
  47. package/src/adapters/openai-responses/tool-output-recovery.ts +9 -3
  48. package/src/adapters/qoder/adapter.ts +15 -12
  49. package/src/adapters/qoder/scaffold-guard.ts +45 -13
  50. package/src/bridge/internal.ts +20 -0
  51. package/src/bridge/sse.ts +14 -5
  52. package/src/claude/agents-inject.ts +2 -1
  53. package/src/claude/desktop-applied-marker.ts +44 -0
  54. package/src/claude/desktop-profile.ts +22 -12
  55. package/src/claude/inbound.ts +8 -3
  56. package/src/cli/access.ts +22 -5
  57. package/src/cli/account-api.ts +6 -0
  58. package/src/cli/account-auth.ts +5 -2
  59. package/src/cli/account.ts +5 -6
  60. package/src/cli/aside-profiles.ts +61 -1
  61. package/src/cli/claude-desktop.ts +3 -2
  62. package/src/cli/claude.ts +3 -1
  63. package/src/cli/codex-cli-update.ts +2 -2
  64. package/src/cli/codex-shim-autorestore.ts +3 -0
  65. package/src/cli/dispatch.ts +12 -4
  66. package/src/cli/index.ts +98 -10
  67. package/src/cli/registry.ts +1 -1
  68. package/src/cli/resolve.ts +113 -1
  69. package/src/cli/root.ts +5 -0
  70. package/src/cli/stop-approval.ts +186 -0
  71. package/src/cli/stop-report.ts +1 -1
  72. package/src/cli/system-restart-client.ts +19 -4
  73. package/src/client/hub-client.ts +25 -18
  74. package/src/clients/config-export.ts +12 -4
  75. package/src/codex/account-auto-switch.ts +59 -0
  76. package/src/codex/account-lifecycle.ts +7 -0
  77. package/src/codex/account-priority.ts +10 -0
  78. package/src/codex/account-usability.ts +9 -0
  79. package/src/codex/auth-api/account-list.ts +5 -0
  80. package/src/codex/auth-api/login-flow.ts +15 -5
  81. package/src/codex/auth-api/login-state.ts +5 -16
  82. package/src/codex/auth-api/pool-mode-gate.ts +30 -7
  83. package/src/codex/auth-api/routes.ts +39 -3
  84. package/src/codex/auth-context.ts +145 -17
  85. package/src/codex/catalog/build-entries.ts +2 -1
  86. package/src/codex/catalog/effort.ts +8 -2
  87. package/src/codex/catalog/metadata.ts +33 -7
  88. package/src/codex/catalog/native-models.ts +102 -5
  89. package/src/codex/catalog/parsing.ts +2 -0
  90. package/src/codex/catalog/provider-models.ts +19 -6
  91. package/src/codex/cli-installation-targets.ts +44 -40
  92. package/src/codex/codex-write-lock.ts +32 -2
  93. package/src/codex/inject/multi-agent-v2.ts +90 -0
  94. package/src/codex/inject/plan.ts +365 -0
  95. package/src/codex/inject.ts +237 -362
  96. package/src/codex/log-guard/maintenance.ts +30 -13
  97. package/src/codex/project-config-warnings.ts +47 -7
  98. package/src/codex/prompt-layers/encoding.ts +41 -0
  99. package/src/codex/prompt-layers/toml-read.ts +1 -1
  100. package/src/codex/prompt-layers.ts +17 -13
  101. package/src/codex/prompt-text-probe.ts +20 -10
  102. package/src/codex/quota-observation-freshness.ts +34 -0
  103. package/src/codex/quota-rejection.ts +8 -4
  104. package/src/codex/quota.ts +4 -2
  105. package/src/codex/routing/selection.ts +32 -9
  106. package/src/codex/routing.ts +53 -11
  107. package/src/codex/subagent-model-fallback.ts +4 -3
  108. package/src/codex/sync.ts +11 -0
  109. package/src/codex/windows-installation-files.ts +33 -11
  110. package/src/codex/write-coordination.ts +4 -0
  111. package/src/combos/request.ts +6 -2
  112. package/src/companion/settings.ts +6 -1
  113. package/src/config/atomic-write.ts +12 -1
  114. package/src/config/derived-registries.ts +29 -0
  115. package/src/config/diagnostics.ts +17 -0
  116. package/src/config/live-reconcile.ts +11 -2
  117. package/src/config/load-degrade.ts +9 -2
  118. package/src/config/persist-unlocked.ts +4 -3
  119. package/src/config/persisted-mutation.ts +94 -0
  120. package/src/config/provider-validation.ts +1 -1
  121. package/src/config/proxy-env.ts +62 -12
  122. package/src/config/rebase-provenance.ts +80 -1
  123. package/src/config/schema/config-schema.ts +5 -0
  124. package/src/config/schema/leaf-validators.ts +50 -1
  125. package/src/config/subagent-models.ts +41 -15
  126. package/src/config.ts +18 -95
  127. package/src/generated/compatibility-version.json +330 -218
  128. package/src/generated/model-metadata.ts +8 -5
  129. package/src/github/star-state.ts +46 -6
  130. package/src/grok/inject.ts +74 -40
  131. package/src/grok/status.ts +11 -4
  132. package/src/integrations/cursor-effort-table.ts +50 -13
  133. package/src/integrations/raycast-detect.ts +19 -4
  134. package/src/integrations/serialize.ts +11 -1
  135. package/src/lib/bounded-body.ts +30 -0
  136. package/src/lib/crash-guard.ts +52 -4
  137. package/src/lib/local-aside-sync-contract.ts +41 -0
  138. package/src/lib/package-tree-integrity.ts +181 -9
  139. package/src/lib/proxy-env.ts +18 -1
  140. package/src/lib/request-failure-attribution.ts +1 -0
  141. package/src/lib/request-failure-model.ts +3 -0
  142. package/src/lib/service-secrets.ts +99 -2
  143. package/src/lib/socks5-fetch.ts +43 -14
  144. package/src/lib/token-estimate.ts +17 -2
  145. package/src/oauth/command-code.ts +3 -2
  146. package/src/oauth/generic-account-failover.ts +29 -1
  147. package/src/oauth/index.ts +24 -16
  148. package/src/oauth/login-flow-state.ts +5 -5
  149. package/src/oauth/meta-muse-device.ts +49 -7
  150. package/src/oauth/store.ts +74 -9
  151. package/src/providers/alibaba-region-backup.ts +16 -1
  152. package/src/providers/anthropic-fast.ts +89 -0
  153. package/src/providers/anthropic-reset-grant-ledger.ts +288 -0
  154. package/src/providers/anthropic-reset-grants.ts +329 -0
  155. package/src/providers/claude-cli-identity.ts +12 -0
  156. package/src/providers/command-code-efforts.ts +184 -102
  157. package/src/providers/derive.ts +11 -3
  158. package/src/providers/fastwire.ts +16 -3
  159. package/src/providers/label.ts +8 -3
  160. package/src/providers/model-rename-fields.ts +1 -0
  161. package/src/providers/openai-virtual-models.ts +1 -0
  162. package/src/providers/quota/vendor-probes-oauth.ts +2 -1
  163. package/src/providers/reasoning-metadata.ts +38 -15
  164. package/src/providers/registry/entries-core.ts +95 -7
  165. package/src/providers/registry/entries-extended.ts +21 -10
  166. package/src/providers/registry/model-ids.ts +1 -0
  167. package/src/providers/registry/model-seeds.ts +31 -3
  168. package/src/providers/registry/types.ts +3 -1
  169. package/src/providers/resolved-model-policy-merge.ts +38 -8
  170. package/src/providers/resolved-model-policy.ts +4 -2
  171. package/src/providers/service-tier.ts +3 -1
  172. package/src/reasoning-effort.ts +6 -5
  173. package/src/responses/code-mode-helper-compat.ts +14 -4
  174. package/src/responses/code-mode-shell-input.ts +54 -0
  175. package/src/responses/custom-tool-compat.ts +173 -5
  176. package/src/responses/parser.ts +38 -2
  177. package/src/responses/reasoning-replay-cache.ts +26 -0
  178. package/src/router.ts +11 -1
  179. package/src/routing/history/indexer.ts +7 -2
  180. package/src/routing/history/schema.ts +3 -1
  181. package/src/server/adapter-resolve.ts +2 -2
  182. package/src/server/auth-cors.ts +3 -0
  183. package/src/server/chat-completions.ts +14 -1
  184. package/src/server/chat-native.ts +17 -0
  185. package/src/server/claude-messages.ts +4 -3
  186. package/src/server/direct-local-http.ts +45 -19
  187. package/src/server/gui-session.ts +6 -15
  188. package/src/server/index/package-tree-guard.ts +53 -0
  189. package/src/server/index/serve-options.ts +17 -3
  190. package/src/server/index/startup-warnings.ts +17 -0
  191. package/src/server/index.ts +15 -17
  192. package/src/server/lifecycle.ts +8 -0
  193. package/src/server/live.ts +54 -4
  194. package/src/server/management/agent-settings-routes.ts +44 -8
  195. package/src/server/management/anthropic-reset-grant-routes.ts +252 -0
  196. package/src/server/management/codex-prompt-routes.ts +5 -1
  197. package/src/server/management/config-routes.ts +14 -4
  198. package/src/server/management/oauth-account-routes.ts +17 -1
  199. package/src/server/management/provider-overwrite-carry.ts +164 -0
  200. package/src/server/management/provider-routes.ts +36 -6
  201. package/src/server/management/remote-workspace-routes.ts +2 -2
  202. package/src/server/management/route-registry.ts +2 -0
  203. package/src/server/management/shadow-call-validation.ts +39 -0
  204. package/src/server/management/system-restart.ts +72 -6
  205. package/src/server/management-api.ts +16 -2
  206. package/src/server/management-auth.ts +52 -8
  207. package/src/server/proxy-liveness.ts +110 -4
  208. package/src/server/request-log.ts +26 -9
  209. package/src/server/request-metrics.ts +5 -0
  210. package/src/server/responses/adapter-continuation.ts +6 -3
  211. package/src/server/responses/adapter-dispatch.ts +65 -1
  212. package/src/server/responses/compact.ts +31 -1
  213. package/src/server/responses/compaction-routing.ts +28 -3
  214. package/src/server/responses/core-codex-account.ts +85 -26
  215. package/src/server/responses/core-combo-failure.ts +16 -15
  216. package/src/server/responses/core-normalize.ts +5 -1
  217. package/src/server/responses/core-opaque-recovery.ts +50 -2
  218. package/src/server/responses/core-options.ts +2 -0
  219. package/src/server/responses/core-replay.ts +7 -0
  220. package/src/server/responses/core.ts +1 -1
  221. package/src/server/responses/encrypted-payload.ts +23 -9
  222. package/src/server/responses/passthrough-delivery.ts +55 -35
  223. package/src/server/responses/passthrough-dispatch.ts +12 -13
  224. package/src/server/responses/policy-fallback.ts +12 -1
  225. package/src/server/responses/request-prepare.ts +46 -7
  226. package/src/server/responses/request-spend.ts +31 -15
  227. package/src/server/responses/run-turn-execution.ts +6 -5
  228. package/src/server/responses/shadow-target-availability.ts +61 -0
  229. package/src/server/responses-custom-tool-repair.ts +2 -0
  230. package/src/service/claim.ts +185 -0
  231. package/src/service/cli.ts +10 -1
  232. package/src/service/guarded-manager-target.ts +151 -0
  233. package/src/service/guards.ts +47 -48
  234. package/src/service/managing-cli.ts +170 -0
  235. package/src/service/orchestration.ts +3 -1
  236. package/src/service/state.ts +4 -0
  237. package/src/service/systemd.ts +16 -1
  238. package/src/service.ts +1 -1
  239. package/src/types/config.ts +8 -0
  240. package/src/types/provider.ts +16 -0
  241. package/src/types/request.ts +10 -0
  242. package/src/types/tools.ts +9 -1
  243. package/src/types/wire.ts +68 -4
  244. package/src/types.ts +1 -0
  245. package/src/update/job.ts +10 -16
  246. package/src/update/npm-invocation.mjs +17 -16
  247. package/src/update/transactional-install.d.mts +22 -1
  248. package/src/update/transactional-install.mjs +176 -19
  249. package/src/update/update-failure-guidance.d.mts +8 -0
  250. package/src/update/update-failure-guidance.mjs +47 -0
  251. package/src/usage/expected-prices.ts +54 -12
  252. package/src/usage/log.ts +117 -3
  253. package/src/usage/telemetry-contract.ts +1 -0
  254. package/src/usage/timeline.ts +34 -6
  255. package/src/usage/user-cost-overlay-reconciler.ts +3 -3
  256. package/src/vision/reasoning.ts +2 -4
  257. package/src/web-search/xai-executor.ts +14 -4
  258. package/gui/dist/assets/App-BqrsSrIR.js +0 -50
  259. package/gui/dist/assets/index-C6SJrh0N.js +0 -86
  260. package/gui/dist/assets/index-DdDunwDb.css +0 -1
  261. package/gui/dist/assets/usage-companion-chart-CzAAAB1o.js +0 -1
  262. package/src/adapters/kiro-thinking.ts +0 -112
@@ -47,6 +47,14 @@ import { fetchQoderModels } from "../../adapters/qoder/live-models";
47
47
  import { resolveQoderProfile } from "../../adapters/qoder/profiles";
48
48
  import { parseAntigravityAvailableModels } from "../../providers/antigravity-models";
49
49
  import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
50
+ import {
51
+ applyProviderCompatPatchFields,
52
+ carryProviderCompatFields,
53
+ providerCompatFieldConfigError,
54
+ providerOverwriteKeepsDestination,
55
+ sampleProviderOverwrite,
56
+ } from "./provider-overwrite-carry";
57
+ import { shadowInterceptProviderDependency } from "./shadow-call-validation";
50
58
  import { deriveProviderPresets, providerConfigSeed } from "../../providers/derive";
51
59
  import { initializeProviderModelSelection } from "../../providers/initial-model-selection";
52
60
  import { effectiveGoogleMode, providerCodexAccountMode, providerMatchesRegistryTransport } from "../../providers/registry";
@@ -111,7 +119,7 @@ import {
111
119
  LOCAL_PROVIDER_RELOAD_NAME_HEADER,
112
120
  LOCAL_PROVIDER_RELOAD_PATH,
113
121
  } from "../../lib/local-provider-reload-contract";
114
- import { refreshUserCostOverlays } from "../../usage/user-cost-overlays";
122
+ import { refreshConfigDerivedRegistries } from "../../config/derived-registries";
115
123
  import { redactSecretString } from "../../lib/redact";
116
124
  import {
117
125
  XAI_RESPONSES_OPT_IN_MODELS,
@@ -742,6 +750,10 @@ function applyProviderPatchFields(
742
750
  }
743
751
  touched = true;
744
752
  }
753
+ // The reasoning-replay lists, foldDeveloperRoleToSystem and reasoningWireFormat (#5563).
754
+ const compat = applyProviderCompatPatchFields(rawBody, next);
755
+ if ("error" in compat) return { error: compat.error };
756
+ if (compat.touched) touched = true;
745
757
 
746
758
  // headers is the one object-valued field in the mask. PATCH semantics merge it
747
759
  // shallowly into the existing block so a single fingerprint header can be added
@@ -1003,7 +1015,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1003
1015
  reconcileLiveStateStores();
1004
1016
  // The complete disk snapshot owns display overlays, including providers that this
1005
1017
  // live routing instance deliberately does not adopt.
1006
- refreshUserCostOverlays(currentDiskConfig);
1018
+ refreshConfigDerivedRegistries(currentDiskConfig);
1007
1019
  clearGatherRoutedModelsInflight();
1008
1020
  (deps.clearProviderQuotaCache ?? clearProviderQuotaCache)();
1009
1021
  clearAccountQuotaCache(name);
@@ -1105,7 +1117,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1105
1117
 
1106
1118
  adoptProviderEditorCandidate(config, outcome.value.config);
1107
1119
  reconcileLiveStateStores();
1108
- refreshUserCostOverlays(outcome.value.config);
1120
+ refreshConfigDerivedRegistries(outcome.value.config);
1109
1121
  clearGatherRoutedModelsInflight();
1110
1122
  (deps.clearProviderQuotaCache ?? clearProviderQuotaCache)();
1111
1123
  clearAccountQuotaCache();
@@ -1140,7 +1152,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1140
1152
  const pinError = applyProviderPinFields(transportCandidate as unknown as OcxProviderConfig, body.provider, existing);
1141
1153
  if (pinError) return jsonResponse({ error: pinError }, 400);
1142
1154
  const providerError = providerManagementConfigError(name, transportCandidate)
1143
- ?? providerEmptyToolOutputConfigError(name, transportCandidate);
1155
+ ?? providerEmptyToolOutputConfigError(name, transportCandidate)
1156
+ ?? providerCompatFieldConfigError(body.provider as Record<string, unknown>);
1144
1157
  if (providerError) return jsonResponse({ error: providerError }, 400);
1145
1158
  const rawProvider = body.provider as Record<string, unknown>;
1146
1159
  if (rawProvider.upstreamWebsocket !== undefined && typeof rawProvider.upstreamWebsocket !== "boolean") {
@@ -1195,12 +1208,17 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1195
1208
  // from "the registry supplied it" either. Without this sample, an unrelated edit that
1196
1209
  // omits the key resurrects the registry default over an operator's explicit `false`.
1197
1210
  const submittedAnnotateEmptyToolOutputs = Object.hasOwn(prov, "annotateEmptyToolOutputs");
1211
+ // And for the compatibility settings, several of which enrichment fills from the registry
1212
+ // seed (#5563); the sample also records whether the request named an auth mode.
1213
+ const overwriteSample = sampleProviderOverwrite(prov);
1198
1214
  enrichProviderFromCatalog(name, prov);
1199
1215
  const { saveConfigPreservingClaudeCode: save } = await import("../../config");
1200
1216
  // Overwriting an existing provider must not drop its multi-key pool: carry it over, then
1201
- // let the (possibly new) apiKey join the pool as the active entry.
1217
+ // let the (possibly new) apiKey join the pool as the active entry. Only while the provider
1218
+ // keeps its destination: those keys were issued for the previous upstream.
1202
1219
  const existingPool = config.providers[name]?.apiKeyPool;
1203
- if (existingPool && !prov.apiKeyPool) prov.apiKeyPool = existingPool;
1220
+ if (existingPool && !prov.apiKeyPool
1221
+ && providerOverwriteKeepsDestination(prov, config.providers[name], overwriteSample)) prov.apiKeyPool = existingPool;
1204
1222
  // The same rule applies to user-configured price overlays: the dashboard's
1205
1223
  // add/edit form does not send modelCosts, so an overwrite must not silently
1206
1224
  // erase hand-edited per-model prices from Logs/Usage estimates.
@@ -1259,6 +1277,10 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1259
1277
  if (!submittedUpstreamWebsocket && existing?.upstreamWebsocket !== undefined) {
1260
1278
  prov.upstreamWebsocket = existing.upstreamWebsocket;
1261
1279
  }
1280
+ // The form sends none of the compatibility settings either (#5563). Read the live row rather
1281
+ // than `existing`, like the alias overlays below: a PATCH that saved one of them while DNS
1282
+ // validation awaited must not be undone. Nothing is carried to a new destination.
1283
+ carryProviderCompatFields(prov, config.providers[name], overwriteSample);
1262
1284
  if (existing?.modelContextWindows) {
1263
1285
  // When the client did send a map, its keys win and the user's other keys survive. When
1264
1286
  // it did not, the stored value is the user's map alone: merging the registry seed in
@@ -1475,6 +1497,10 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1475
1497
  // mask onto the newest provider under the mutation lock right before saving, so two
1476
1498
  // concurrent PATCHes updating different fields/headers both survive instead of the
1477
1499
  // later save clobbering the earlier snapshot.
1500
+ // Read before the save: once the provider is disabled the target no longer resolves (#5618).
1501
+ const shadowDependency = rawBody.disabled === true && config.providers[name]!.disabled !== true
1502
+ ? shadowInterceptProviderDependency(config, name)
1503
+ : null;
1478
1504
  let replayError: string | undefined;
1479
1505
  withConfigMutationLockSync(() => {
1480
1506
  const replay = applyProviderPatchFields(name, config.providers[name]!, rawBody, keys, config);
@@ -1540,6 +1566,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1540
1566
  name,
1541
1567
  disabled: config.providers[name]!.disabled === true,
1542
1568
  hasApiKey: !!config.providers[name]!.apiKey,
1569
+ ...(shadowDependency ? { dependentShadowIntercept: shadowDependency } : {}),
1543
1570
  ...(name === "xai"
1544
1571
  ? { xaiResponsesOptInState: xaiResponsesOptInState(config.providers[name]!) }
1545
1572
  : {}),
@@ -1753,6 +1780,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1753
1780
  }, 409);
1754
1781
  }
1755
1782
  const { saveConfigPreservingClaudeCode: save } = await import("../../config");
1783
+ // Deleting still succeeds; the response names the shadow-call target left without a provider.
1784
+ const shadowDependency = shadowInterceptProviderDependency(config, name);
1756
1785
  if (fallbackDefault) config.defaultProvider = fallbackDefault;
1757
1786
  delete config.providers[name];
1758
1787
  const { dropProviderCustomModels } = await import("../../providers/provider-id-rewrite");
@@ -1768,6 +1797,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1768
1797
  success: true,
1769
1798
  ...(fallbackDefault ? { defaultProvider: fallbackDefault } : {}),
1770
1799
  ...(droppedCustomModels > 0 ? { droppedCustomModels } : {}),
1800
+ ...(shadowDependency ? { dependentShadowIntercept: shadowDependency } : {}),
1771
1801
  catalogRefresh,
1772
1802
  });
1773
1803
  }
@@ -10,9 +10,9 @@ function response(body: unknown, status = 200): Response {
10
10
  }
11
11
 
12
12
  function sessionOnly(ctx: ManagementContext): Response | null {
13
- return ctx.principal === "gui-session"
13
+ return ctx.principal === "gui-session" && ctx.sessionControl?.isPaired(ctx.req, ctx.config) === true
14
14
  ? null
15
- : response({ error: "A dashboard session is required for Remote Workspace changes." }, 403);
15
+ : response({ error: "A paired dashboard session is required for Remote Workspace changes." }, 403);
16
16
  }
17
17
 
18
18
  async function jsonObject(req: Request): Promise<Record<string, unknown>> {
@@ -148,6 +148,8 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
148
148
  { method: "POST", path: "/api/grok/apply", module: "server/management/agent-settings-routes", mutates: true },
149
149
  { method: "GET", path: "/api/grok/reset-coupons", module: "server/management/grok-coupon-routes", mutates: false },
150
150
  { method: "POST", path: "/api/grok/reset-coupons/consume", module: "server/management/grok-coupon-routes", mutates: true },
151
+ { method: "GET", path: "/api/anthropic/reset-grants", module: "server/management/anthropic-reset-grant-routes", mutates: false, exempt: { reason: "deferred-verb", why: "Reading Claude reset grants has no CLI verb yet; the dashboard ticket badge is the only reader in this unit.", owner: "260923_claude_reset_grants wp1", ownerDoc: "devlog/_plan/260923_claude_reset_grants/010_plan.md" } },
152
+ { method: "POST", path: "/api/anthropic/reset-grants/consume", module: "server/management/anthropic-reset-grant-routes", mutates: true, exempt: { reason: "session-only", why: "Spending a Claude reset grant requires the gui-session principal (anthropic-reset-grant-routes.ts handleConsume); the admin token is refused." } },
151
153
  { method: "PUT", path: "/api/claude-code", module: "server/management/agent-settings-routes", mutates: true },
152
154
  { method: "PUT", path: "/api/claude-desktop", module: "server/management/agent-settings-routes", mutates: true },
153
155
  { method: "PUT", path: "/api/codex-auth/features/default-mode-request-user-input", module: "server/management/agent-settings-routes", mutates: true },
@@ -13,6 +13,11 @@ export function shadowCallTargetError(config: OcxConfig, targetModel: string | u
13
13
  } catch {
14
14
  return "model must resolve to a configured provider";
15
15
  }
16
+ // A qualified target that only the router's last-resort default-provider fallback accepts names
17
+ // nothing configured; saving it would leave the same dead target the request path refuses (#5618).
18
+ if (target.routeKind === "default-provider" && targetModel.includes("/")) {
19
+ return "model must resolve to a configured provider";
20
+ }
16
21
 
17
22
  const intersectsSource = shadowSourceModels(config.shadowCallIntercept?.sourceModels).some(sourceModel => {
18
23
  let source = { providerName: OPENAI_CODEX_PROVIDER_ID, modelId: sourceModel };
@@ -27,3 +32,37 @@ export function shadowCallTargetError(config: OcxConfig, targetModel: string | u
27
32
  ? "shadow-call target must not intersect a source model"
28
33
  : null;
29
34
  }
35
+
36
+ /** The shadow-call reference a provider change leaves behind, reported by disable and delete. */
37
+ export interface ShadowInterceptDependency {
38
+ model: string;
39
+ enabled: boolean;
40
+ }
41
+
42
+ /**
43
+ * Whether `shadowCallIntercept.model` depends on `providerName` (#5618). Call it before the
44
+ * mutation. A `provider/model` or `alias/model` target naming the provider counts, and so does
45
+ * any other target the router currently resolves to it. Combo and routing-profile targets do not:
46
+ * their pickers already skip a disabled or missing member, and deleting a provider a combo uses is
47
+ * refused outright.
48
+ */
49
+ export function shadowInterceptProviderDependency(config: OcxConfig, providerName: string): ShadowInterceptDependency | null {
50
+ const shadow = config.shadowCallIntercept;
51
+ const model = shadow?.model?.trim();
52
+ if (!model) return null;
53
+ const dependency = { model, enabled: shadow?.enabled === true };
54
+ const slash = model.indexOf("/");
55
+ if (slash > 0) {
56
+ const prefix = model.slice(0, slash).toLowerCase();
57
+ const alias = config.providers[providerName]?.alias?.trim().toLowerCase();
58
+ if (prefix === providerName.toLowerCase() || (alias && prefix === alias)) return dependency;
59
+ }
60
+ let target;
61
+ try {
62
+ target = routeModel(config, model);
63
+ } catch {
64
+ return null;
65
+ }
66
+ if (target.routeKind === "combo" || target.routeKind === "policy") return null;
67
+ return target.providerName === providerName ? dependency : null;
68
+ }
@@ -23,6 +23,7 @@
23
23
  */
24
24
  import { spawn } from "node:child_process";
25
25
  import {
26
+ acquireTemporaryDrain,
26
27
  beginShutdownDrain,
27
28
  drainAndShutdown,
28
29
  getActiveTurnCount,
@@ -54,6 +55,7 @@ export interface ReplacementReadinessIo {
54
55
  }
55
56
 
56
57
  export interface SystemRestartIo {
58
+ acquireTemporaryDrain?: () => { release(): void } | null;
57
59
  drainAndShutdown?: typeof drainAndShutdown;
58
60
  /** True when a background service can actually respawn this process after exit(1). */
59
61
  isServiceViable?: () => boolean;
@@ -75,6 +77,13 @@ export interface SystemRestartIo {
75
77
  now?: () => number;
76
78
  }
77
79
 
80
+ export interface SystemRestartAdmission {
81
+ /** Only the caller that created this pending restart receives its veto. */
82
+ onAccepted?: (veto: () => void) => void;
83
+ /** Recheck external authority before draining or handing off. */
84
+ beforeScheduledDrain?: () => boolean;
85
+ }
86
+
78
87
  let restartIo: SystemRestartIo = {};
79
88
  /** Prevents double-scheduling in the 200ms window before drainAndShutdown sets draining. */
80
89
  let restartAccepted = false;
@@ -135,10 +144,21 @@ function waitForBoundedSettlement(
135
144
  });
136
145
  }
137
146
 
147
+ /**
148
+ * Set when an operator-initiated shutdown (signal or management stop) begins. An automatic
149
+ * restart that is already draining cannot tell its own listener stop from an independent one,
150
+ * so it consults this instead and never hands off after the process was asked to stop.
151
+ */
152
+ let explicitShutdownRequested = false;
153
+ export function noteExplicitShutdownRequested(): void {
154
+ explicitShutdownRequested = true;
155
+ }
156
+
138
157
  /** Test seam — reset between tests. */
139
158
  export function setSystemRestartIoForTests(io: SystemRestartIo = {}): void {
140
159
  restartIo = io;
141
160
  restartAccepted = false;
161
+ explicitShutdownRequested = false;
142
162
  }
143
163
 
144
164
  function resolveListenPort(): number | undefined {
@@ -297,7 +317,9 @@ async function completeDeferredParentExitHandoff(
297
317
  exitProcess: (code: number) => void,
298
318
  port: number | undefined,
299
319
  phase: "deadline" | "listener-stop fallback",
320
+ canHandoff: () => boolean = () => true,
300
321
  ): Promise<void> {
322
+ if (!canHandoff()) return;
301
323
  try {
302
324
  await (io.spawnStart ?? spawnDetachedStart)(port, false);
303
325
  } catch (err) {
@@ -317,10 +339,13 @@ async function completeDeadlineRestartHandoff(
317
339
  exitProcess: (code: number) => void,
318
340
  port: number | undefined,
319
341
  scheduleDeadline: NonNullable<SystemRestartIo["scheduleDeadline"]>,
342
+ canHandoff: () => boolean = () => true,
320
343
  ): Promise<void> {
344
+ if (!canHandoff()) return;
321
345
  const supervised = (io.isSupervisedServiceChild ?? (() => isSupervisedServiceChild(io)))();
322
346
  if (supervised) {
323
347
  // Failure-only supervisors ignore exit(0); intentional non-zero triggers respawn.
348
+ if (!canHandoff()) return;
324
349
  exitProcess(1);
325
350
  return;
326
351
  }
@@ -340,7 +365,7 @@ async function completeDeadlineRestartHandoff(
340
365
  }
341
366
  // The ordinary child must survive parent exit: a failed/pending socket close
342
367
  // or overdue cleanup is completed by process teardown, without a hidden mode.
343
- await completeDeferredParentExitHandoff(io, exitProcess, port, "deadline");
368
+ await completeDeferredParentExitHandoff(io, exitProcess, port, "deadline", canHandoff);
344
369
  }
345
370
 
346
371
  /**
@@ -348,7 +373,7 @@ async function completeDeadlineRestartHandoff(
348
373
  * respawn runs on a short timer so the HTTP response can flush first.
349
374
  * Idempotent while already draining: returns the accepted shape again.
350
375
  */
351
- export function acceptSystemRestart(io: SystemRestartIo = restartIo): {
376
+ export function acceptSystemRestart(io: SystemRestartIo = restartIo, admission: SystemRestartAdmission = {}): {
352
377
  accepted: true;
353
378
  alreadyDraining: boolean;
354
379
  activeTurnCount: number;
@@ -363,13 +388,50 @@ export function acceptSystemRestart(io: SystemRestartIo = restartIo): {
363
388
 
364
389
  if (!alreadyDraining) {
365
390
  restartAccepted = true;
391
+ const automatic = Boolean(admission.onAccepted);
392
+ const temporaryDrain = automatic
393
+ ? (io.acquireTemporaryDrain ?? (() => acquireTemporaryDrain("automatic-restart")))()
394
+ : null;
395
+ const releasePending = () => {
396
+ temporaryDrain?.release();
397
+ restartAccepted = false;
398
+ };
399
+ if (automatic && !temporaryDrain) {
400
+ restartAccepted = false;
401
+ return { accepted: true, alreadyDraining: true, activeTurnCount, drainTimeoutMs: MEMORY_DRAIN_RESTART_MS };
402
+ }
403
+ let pending = true;
404
+ let vetoed = false;
405
+ admission.onAccepted?.(() => {
406
+ if (!pending) return;
407
+ pending = false;
408
+ vetoed = true;
409
+ releasePending();
410
+ });
366
411
  const now = io.now ?? Date.now;
367
412
  const restartDeadlineMs = now() + MEMORY_DRAIN_RESTART_MS;
368
413
  // Reject new data-plane traffic immediately (503), before the 200ms response-flush delay.
369
- if (io.beginShutdownDrain) io.beginShutdownDrain();
370
- else if (io.setDraining) io.setDraining(true);
371
- else beginShutdownDrain();
414
+ if (!automatic) {
415
+ if (io.beginShutdownDrain) io.beginShutdownDrain();
416
+ else if (io.setDraining) io.setDraining(true);
417
+ else beginShutdownDrain();
418
+ }
372
419
  schedule(async () => {
420
+ if (vetoed) return;
421
+ pending = false;
422
+ const canHandoff = () => {
423
+ // Only admission-bound (automatic) restarts yield to an explicit stop; manual restart
424
+ // requests keep their existing semantics.
425
+ if (admission.onAccepted && explicitShutdownRequested) return false;
426
+ try { return admission.beforeScheduledDrain?.() ?? true; }
427
+ catch { return false; } // Unknown ownership is not authority to restart.
428
+ };
429
+ if (!canHandoff()) { releasePending(); return; }
430
+ if (automatic) {
431
+ if (io.beginShutdownDrain) io.beginShutdownDrain();
432
+ else if (!io.setDraining) beginShutdownDrain();
433
+ temporaryDrain?.release();
434
+ }
373
435
  // Preserve the live binding before drainAndShutdown (or its deadline race)
374
436
  // closes the listener and makes both the server ref and runtime metadata stale.
375
437
  const restartPort = (io.listenPort ?? resolveListenPort)();
@@ -381,10 +443,11 @@ export function acceptSystemRestart(io: SystemRestartIo = restartIo): {
381
443
  return () => clearTimeout(timer);
382
444
  });
383
445
  const drainOutcome = await waitForRestartDrain(drainPromise, restartDeadlineMs, now, scheduleDeadline);
446
+ if (!canHandoff()) return;
384
447
  const exitProcess = io.exitProcess ?? ((code: number) => { process.exit(code); });
385
448
  if (drainOutcome === "deadline") {
386
449
  console.warn("Drain-and-restart deadline expired; forcing terminal restart handoff");
387
- await completeDeadlineRestartHandoff(io, exitProcess, restartPort, scheduleDeadline);
450
+ await completeDeadlineRestartHandoff(io, exitProcess, restartPort, scheduleDeadline, canHandoff);
388
451
  return;
389
452
  }
390
453
  if (drainOutcome === "failed" || drainOutcome === "rejected") {
@@ -395,6 +458,7 @@ export function acceptSystemRestart(io: SystemRestartIo = restartIo): {
395
458
  const supervised = (io.isSupervisedServiceChild ?? (() => isSupervisedServiceChild(io)))();
396
459
  if (supervised) {
397
460
  // Failure-only supervisors ignore exit(0); intentional non-zero triggers respawn.
461
+ if (!canHandoff()) return;
398
462
  (io.exitProcess ?? ((code: number) => { process.exit(code); }))(1);
399
463
  return;
400
464
  }
@@ -407,10 +471,12 @@ export function acceptSystemRestart(io: SystemRestartIo = restartIo): {
407
471
  exitProcess,
408
472
  restartPort,
409
473
  "listener-stop fallback",
474
+ canHandoff,
410
475
  );
411
476
  return;
412
477
  }
413
478
  try {
479
+ if (!canHandoff()) return;
414
480
  // A rejected drain has uncertain cleanup ownership, so it uses the same
415
481
  // parent-exit handoff as a deadline. Only a fully completed drain waits
416
482
  // for replacement health in the old process.
@@ -50,6 +50,7 @@ import {
50
50
  import type { OcxClaudeCodeConfig, OcxClaudeDesktopProfile, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../types";
51
51
  import type { DesktopProfileModel } from "../claude/desktop-profile";
52
52
  import { drainAndShutdown } from "./lifecycle";
53
+ import { noteExplicitShutdownRequested } from "./management/system-restart";
53
54
  import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "./request-log";
54
55
  import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../usage/cost";
55
56
  import type { PersistedUsageAttempt } from "../usage/log";
@@ -159,6 +160,12 @@ async function handleGrokCouponRoutesOnDemand(ctx: ManagementContext): Promise<R
159
160
  return handleGrokCouponRoutes(ctx);
160
161
  }
161
162
 
163
+ async function handleAnthropicResetGrantRoutesOnDemand(ctx: ManagementContext): Promise<Response | null> {
164
+ if (!pathInManagementNamespace(ctx.url.pathname, "/api/anthropic/reset-grants", true)) return null;
165
+ const { handleAnthropicResetGrantRoutes } = await import("./management/anthropic-reset-grant-routes");
166
+ return handleAnthropicResetGrantRoutes(ctx);
167
+ }
168
+
162
169
  async function handleRemoteWorkspaceRoutesOnDemand(ctx: ManagementContext): Promise<Response | null> {
163
170
  if (!pathInManagementNamespace(ctx.url.pathname, "/api/remote-workspace")) return null;
164
171
  if (!remoteWorkspaceEnabled(ctx.config)) {
@@ -166,8 +173,11 @@ async function handleRemoteWorkspaceRoutesOnDemand(ctx: ManagementContext): Prom
166
173
  status: ctx.req.method === "GET" ? 200 : 404, headers: { "cache-control": "no-store" },
167
174
  });
168
175
  }
169
- if (ctx.req.method !== "GET" && ctx.principal !== "gui-session") {
170
- return Response.json({ error: "A dashboard session is required for Remote Workspace changes." }, { status: 403 });
176
+ if (ctx.req.method !== "GET" && (
177
+ ctx.principal !== "gui-session"
178
+ || ctx.sessionControl?.isPaired(ctx.req, ctx.config) !== true
179
+ )) {
180
+ return Response.json({ error: "A paired dashboard session is required for Remote Workspace changes." }, { status: 403 });
171
181
  }
172
182
  const { handleRemoteWorkspaceRoutes } = await import("./management/remote-workspace-routes");
173
183
  return handleRemoteWorkspaceRoutes(ctx);
@@ -274,6 +284,7 @@ export async function handleManagementAPI(
274
284
  ?? (await handleQuotaResetRoutesOnDemand(ctx))
275
285
  ?? (await handleWorkflowBudgetRoutesOnDemand(ctx))
276
286
  ?? (await handleGrokCouponRoutesOnDemand(ctx))
287
+ ?? (await handleAnthropicResetGrantRoutesOnDemand(ctx))
277
288
  ?? handleMetricsRoutes(ctx)
278
289
  ?? (await handleRoutingAnalyticsRoutes(ctx))
279
290
  ?? (await handleRoutingProfileRoutesOnDemand(ctx))
@@ -395,6 +406,9 @@ export async function handleManagementAPI(
395
406
  // syncCleanup skips this when OCX_SERVICE is set (so a crash/respawn keeps the fence),
396
407
  // which is exactly why an intentional stop has to do it here — unless the caller is
397
408
  // `ocx stop`, which does it itself once the proxy is proven down.
409
+ // Mark the stop before the first await after acceptance, so an automatic restart draining
410
+ // concurrently cannot reach its handoff while teardown is still pending.
411
+ noteExplicitShutdownRequested();
398
412
  const teardown = await performStopTeardown(url, { ownsReceipt: deferralMatchesReceipt });
399
413
  setTimeout(async () => {
400
414
  let shutdownSucceeded = false;
@@ -1,4 +1,13 @@
1
1
  import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
2
+ import {
3
+ LOCAL_ASIDE_SYNC_CAPABILITY_HEADER,
4
+ LOCAL_ASIDE_SYNC_EXPECTED_PID_HEADER,
5
+ LOCAL_ASIDE_SYNC_EXPIRES_AT_HEADER,
6
+ LOCAL_ASIDE_SYNC_NONCE_HEADER,
7
+ LOCAL_ASIDE_SYNC_PATH,
8
+ parseExpectedLocalAsideSyncPid,
9
+ verifyLocalAsideSyncCapability,
10
+ } from "../lib/local-aside-sync-contract";
2
11
  import {
3
12
  chmodSync,
4
13
  closeSync,
@@ -70,6 +79,8 @@ const admittedLocalReadRequests = new WeakSet<Request>();
70
79
  const LOCAL_PROVIDER_RELOAD_REPLAY_LIMIT = 256;
71
80
  const consumedLocalProviderReloadCapabilities = new Map<string, number>();
72
81
  const admittedLocalProviderReloadRequests = new WeakSet<Request>();
82
+ const consumedLocalAsideSyncCapabilities = new Map<string, number>();
83
+ const admittedLocalAsideSyncRequests = new WeakSet<Request>();
73
84
  const GUI_PAIR_REPLAY_LIMIT = 256;
74
85
  const consumedGuiPairCapabilities = new Map<string, number>();
75
86
  const admittedGuiPairRequests = new WeakSet<Request>();
@@ -253,23 +264,35 @@ export interface ManagementSessionControl {
253
264
  revokeCurrent(req: Request): boolean;
254
265
  /** Revalidate a long-lived request against current authority, without cached admission or renewal. */
255
266
  isCurrent(req: Request, config: OcxConfig): boolean;
267
+ /** Prove that the current browser session came from the operator-mediated pairing flow. */
268
+ isPaired(req: Request, config: OcxConfig): boolean;
256
269
  }
257
270
 
258
271
  export function createManagementSessionControl(state: ManagementAuthState): ManagementSessionControl {
272
+ function currentSession(req: Request, config: OcxConfig): GuiSessionRecord | null {
273
+ if (!state.available) return null;
274
+ const adminToken = state.token;
275
+ const credential = requestManagementCredential(req);
276
+ if (!credential || equalSecret(credential, adminToken)) return null;
277
+ const session = state.sessions.get(credential);
278
+ if (!session) return null;
279
+ // Reuse the full origin/expiry/CSRF predicate against the current record, but
280
+ // isolate its sliding-expiry mutation: authority checks are not browser activity.
281
+ return authorizeGuiSessionRequest(req, config, {
282
+ sessions: new Map([[credential, { ...session }]]),
283
+ pairingGrants: state.pairingGrants,
284
+ }).ok ? session : null;
285
+ }
259
286
  return {
260
287
  isCurrent(req: Request, config: OcxConfig): boolean {
261
288
  if (!state.available) return false;
262
289
  const credential = requestManagementCredential(req);
263
290
  if (!credential) return false;
264
291
  if (equalSecret(credential, state.token)) return true;
265
- const session = state.sessions.get(credential);
266
- if (!session) return false;
267
- // Reuse the full origin/expiry/CSRF predicate against the current record, but
268
- // isolate its sliding-expiry mutation: SSE heartbeats are not browser activity.
269
- return authorizeGuiSessionRequest(req, config, {
270
- sessions: new Map([[credential, { ...session }]]),
271
- pairingGrants: state.pairingGrants,
272
- }).ok;
292
+ return currentSession(req, config) !== null;
293
+ },
294
+ isPaired(req: Request, config: OcxConfig): boolean {
295
+ return currentSession(req, config)?.issuance === "pairing";
273
296
  },
274
297
  revokeCurrent(req: Request): boolean {
275
298
  if (!state.available) return false;
@@ -302,6 +325,7 @@ export type ManagementPrincipal =
302
325
  | "gui-pair-capability"
303
326
  | "local-read-capability"
304
327
  | "local-provider-reload-capability"
328
+ | "local-aside-sync-capability"
305
329
  | "system-restart-capability";
306
330
 
307
331
  export interface LocalManagementAuthContext {
@@ -432,6 +456,25 @@ function hasLocalProviderReloadCapability(
432
456
  return true;
433
457
  }
434
458
 
459
+ function hasLocalAsideSyncCapability(req: Request, local: LocalManagementAuthContext | undefined): boolean {
460
+ if (admittedLocalAsideSyncRequests.has(req)) return true;
461
+ if (!local || req.method !== "POST") return false;
462
+ let url: URL;
463
+ try { url = new URL(req.url); } catch { return false; }
464
+ if (url.pathname !== LOCAL_ASIDE_SYNC_PATH || url.search !== "") return false;
465
+ if (parseExpectedLocalAsideSyncPid(req.headers.get(LOCAL_ASIDE_SYNC_EXPECTED_PID_HEADER)) !== local.pid) return false;
466
+ const expiresAt = Number(req.headers.get(LOCAL_ASIDE_SYNC_EXPIRES_AT_HEADER));
467
+ if (!Number.isSafeInteger(expiresAt)) return false;
468
+ const capability = req.headers.get(LOCAL_ASIDE_SYNC_CAPABILITY_HEADER);
469
+ const now = Date.now();
470
+ if (!verifyLocalAsideSyncCapability(local.attestationSecret, req.headers.get(LOCAL_ASIDE_SYNC_NONCE_HEADER), req.method, url.pathname, local.pid, local.port, expiresAt, capability, now)) return false;
471
+ for (const [used, until] of consumedLocalAsideSyncCapabilities) if (until <= now) consumedLocalAsideSyncCapabilities.delete(used);
472
+ if (!capability || consumedLocalAsideSyncCapabilities.has(capability) || consumedLocalAsideSyncCapabilities.size >= 256) return false;
473
+ consumedLocalAsideSyncCapabilities.set(capability, expiresAt);
474
+ admittedLocalAsideSyncRequests.add(req);
475
+ return true;
476
+ }
477
+
435
478
  function hasGuiPairCapability(
436
479
  req: Request,
437
480
  local: LocalManagementAuthContext | undefined,
@@ -505,6 +548,7 @@ function resolveManagementAdmission(
505
548
  if (cached) return cached;
506
549
  let principal: ManagementPrincipal | null = null;
507
550
  if (hasSystemRestartCapability(req, local)) principal = "system-restart-capability";
551
+ else if (hasLocalAsideSyncCapability(req, local)) principal = "local-aside-sync-capability";
508
552
  else if (hasLocalProviderReloadCapability(req, local)) principal = "local-provider-reload-capability";
509
553
  else if (hasLocalReadCapability(req, local)) principal = "local-read-capability";
510
554
  else if (hasGuiPairCapability(req, local)) principal = "gui-pair-capability";