@bitkyc08/opencodex 2.79.0 → 2.81.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/App-7yhoys8a.js +54 -0
  3. package/gui/dist/assets/Tray-EwAOsP0S.js +1 -0
  4. package/gui/dist/assets/index-RsoaAxXY.js +94 -0
  5. package/gui/dist/assets/quota-summary-D2lN8tDm.js +1 -0
  6. package/gui/dist/index.html +1 -1
  7. package/gui/dist/provider-icons/claude-green.svg +1 -0
  8. package/package.json +3 -2
  9. package/src/adapters/anthropic-model-contract.ts +8 -2
  10. package/src/adapters/anthropic.ts +7 -2
  11. package/src/adapters/codebuddy/mcp-server.ts +2 -25
  12. package/src/adapters/codebuddy/tool-bridge.ts +1 -7
  13. package/src/adapters/coding-agent/mcp-server.ts +33 -0
  14. package/src/adapters/coding-agent/protocol.ts +122 -17
  15. package/src/adapters/coding-agent/tool-catalog.ts +75 -0
  16. package/src/adapters/coding-agent/turn.ts +101 -44
  17. package/src/adapters/cursor/catalog.ts +8 -0
  18. package/src/adapters/cursor/effort-map.ts +2 -0
  19. package/src/adapters/cursor/live-transport.ts +5 -3
  20. package/src/adapters/cursor/native-exec-fs.ts +12 -9
  21. package/src/adapters/cursor/native-exec.ts +13 -5
  22. package/src/adapters/cursor/protobuf-request.ts +13 -7
  23. package/src/adapters/cursor/request-builder.ts +23 -10
  24. package/src/adapters/cursor/tool-definitions.ts +9 -6
  25. package/src/adapters/cursor/tool-guidance.ts +29 -8
  26. package/src/adapters/cursor/tool-naming.ts +40 -7
  27. package/src/adapters/cursor/tool-wording.ts +46 -0
  28. package/src/adapters/cursor.ts +3 -0
  29. package/src/adapters/devin/live-models.ts +2 -0
  30. package/src/adapters/devin.ts +25 -11
  31. package/src/adapters/kiro-retry.ts +9 -4
  32. package/src/adapters/openai-chat/messages.ts +7 -4
  33. package/src/adapters/physical-send.ts +8 -5
  34. package/src/adapters/qoder/adapter.ts +41 -6
  35. package/src/adapters/qoder/scaffold-guard.ts +2 -3
  36. package/src/adapters/zed.ts +16 -1
  37. package/src/claude/inbound-cache-stabilize.ts +4 -9
  38. package/src/claude/inbound.ts +10 -0
  39. package/src/claude/intercept/model-bindings.ts +1 -0
  40. package/src/claude/intercept/picker-listener.ts +71 -4
  41. package/src/cli/account-anthropic-threshold.ts +14 -5
  42. package/src/cli/account-api.ts +3 -1
  43. package/src/cli/account-extended.ts +13 -9
  44. package/src/cli/account-policy-dto.ts +1 -1
  45. package/src/cli/account-policy.ts +13 -4
  46. package/src/cli/account.ts +3 -3
  47. package/src/cli/capabilities-accounts.ts +2 -2
  48. package/src/cli/capabilities-agents-routing.ts +24 -0
  49. package/src/cli/capabilities-integrations.ts +16 -0
  50. package/src/cli/codex-shim-autorestore.ts +3 -0
  51. package/src/cli/dispatch.ts +10 -1
  52. package/src/cli/help.ts +4 -1
  53. package/src/cli/index.ts +11 -5
  54. package/src/cli/integration-plan-dto.ts +1 -0
  55. package/src/cli/integration-preview.ts +8 -1
  56. package/src/cli/integrations.ts +32 -3
  57. package/src/cli/message-args.ts +34 -0
  58. package/src/cli/message-command.ts +43 -0
  59. package/src/cli/message-runtime.ts +6 -0
  60. package/src/cli/observe.ts +3 -1
  61. package/src/cli/provider.ts +29 -6
  62. package/src/cli/registry.ts +24 -2
  63. package/src/clients/config-export/commandcode.ts +131 -0
  64. package/src/clients/config-export/contracts.ts +12 -0
  65. package/src/clients/config-export.ts +64 -1
  66. package/src/codex/auth-api/account-list.ts +11 -3
  67. package/src/codex/auth-api/main-account-probe.ts +25 -8
  68. package/src/codex/auth-api/pool-mode-gate.ts +33 -9
  69. package/src/codex/auth-api/pool-quota-probe.ts +15 -1
  70. package/src/codex/auth-api/routes.ts +1 -0
  71. package/src/codex/autostart-health.ts +3 -3
  72. package/src/codex/catalog/auto-review.ts +13 -6
  73. package/src/codex/catalog/build-entries.ts +15 -7
  74. package/src/codex/catalog/control-plane.ts +45 -0
  75. package/src/codex/catalog/effort.ts +11 -1
  76. package/src/codex/catalog/gather-capture.ts +5 -3
  77. package/src/codex/catalog/parsing.ts +2 -1
  78. package/src/codex/catalog/provider-models.ts +15 -9
  79. package/src/codex/control-plane-models.ts +6 -0
  80. package/src/codex/journal.ts +36 -5
  81. package/src/codex/shim-overlay.ts +9 -1
  82. package/src/combos/jev-decision-contract.ts +25 -2
  83. package/src/combos/jev.ts +6 -3
  84. package/src/combos/types.ts +1 -1
  85. package/src/config/diagnostics.ts +27 -16
  86. package/src/config/load-degrade.ts +34 -0
  87. package/src/config/schema/anthropic-account-pool.ts +143 -0
  88. package/src/config/schema/config-schema.ts +7 -1
  89. package/src/config/schema/leaf-validators.ts +4 -0
  90. package/src/config.ts +2 -0
  91. package/src/generated/compatibility-version.json +347 -199
  92. package/src/generated/model-metadata.ts +5 -4
  93. package/src/images/loop.ts +2 -1
  94. package/src/integrations/catalog-refresh.ts +1 -1
  95. package/src/integrations/mutation-plan.ts +7 -0
  96. package/src/integrations/registry.ts +8 -0
  97. package/src/integrations/state.ts +9 -2
  98. package/src/integrations/writer.ts +8 -2
  99. package/src/lib/lab-live-route-production.ts +9 -1
  100. package/src/lib/request-execution-budget.ts +236 -58
  101. package/src/lib/spend-pool-alias-validation.ts +79 -0
  102. package/src/lib/spend-pool-continuity.ts +34 -0
  103. package/src/lib/spend-reservation-ledger.ts +412 -50
  104. package/src/lib/state-store-registrations.ts +20 -3
  105. package/src/lib/upstream-retry.ts +17 -5
  106. package/src/lib/workflow-budget.ts +53 -11
  107. package/src/messaging/budget.ts +45 -0
  108. package/src/messaging/discovery.ts +57 -0
  109. package/src/messaging/envelope.ts +49 -0
  110. package/src/messaging/input.ts +37 -0
  111. package/src/messaging/rpc.ts +196 -0
  112. package/src/messaging/send.ts +58 -0
  113. package/src/messaging/socket.ts +72 -0
  114. package/src/messaging/types.ts +29 -0
  115. package/src/oauth/anthropic-account-refusal.ts +51 -17
  116. package/src/oauth/anthropic-account-threshold.ts +8 -2
  117. package/src/oauth/anthropic-continuity.ts +15 -1
  118. package/src/oauth/anthropic-model-quota.ts +120 -48
  119. package/src/oauth/anthropic-model-routes.ts +11 -3
  120. package/src/oauth/anthropic-oauth-definitions.ts +98 -0
  121. package/src/oauth/anthropic-pool-config.ts +30 -0
  122. package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
  123. package/src/oauth/anthropic-routing.ts +1202 -866
  124. package/src/oauth/anthropic-send-ownership.ts +41 -0
  125. package/src/oauth/anthropic.ts +4 -1
  126. package/src/oauth/generic-account-failover.ts +3 -1
  127. package/src/oauth/health.ts +8 -6
  128. package/src/oauth/index.ts +90 -173
  129. package/src/oauth/kiro.ts +22 -9
  130. package/src/oauth/login-cli.ts +2 -0
  131. package/src/oauth/model-discovery-auth.ts +43 -0
  132. package/src/oauth/pool-kernel.ts +11 -5
  133. package/src/oauth/pool-settings-capability.ts +5 -3
  134. package/src/oauth/store-anthropic-instance.ts +87 -0
  135. package/src/oauth/store.ts +67 -9
  136. package/src/oauth/token-guardian.ts +2 -0
  137. package/src/oauth/types.ts +2 -0
  138. package/src/protocols/plan-snapshot.ts +5 -0
  139. package/src/protocols/settings.ts +20 -5
  140. package/src/providers/anthropic-instance-id.ts +47 -0
  141. package/src/providers/anthropic-instance.ts +48 -0
  142. package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
  143. package/src/providers/derive.ts +2 -0
  144. package/src/providers/kiro-models.ts +3 -0
  145. package/src/providers/label.ts +1 -1
  146. package/src/providers/model-rename-fields.ts +2 -0
  147. package/src/providers/quota/account-cache.ts +37 -11
  148. package/src/providers/quota/anthropic-account-quota.ts +93 -0
  149. package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
  150. package/src/providers/quota/report-cache.ts +24 -1
  151. package/src/providers/quota/vendor-probes-oauth.ts +46 -14
  152. package/src/providers/quota.ts +22 -32
  153. package/src/providers/registry/entries-core.ts +35 -30
  154. package/src/providers/registry/model-ids.ts +1 -0
  155. package/src/providers/registry/model-seeds.ts +7 -4
  156. package/src/providers/registry/types.ts +3 -0
  157. package/src/providers/registry.ts +3 -3
  158. package/src/responses/spill-store.ts +5 -1
  159. package/src/responses/state/metrics.ts +87 -0
  160. package/src/responses/state/snapshot-codec.ts +6 -0
  161. package/src/responses/state/spill-queue.ts +1 -0
  162. package/src/responses/state/unforced-store-false.ts +55 -0
  163. package/src/responses/state.ts +50 -66
  164. package/src/router.ts +28 -4
  165. package/src/routing/compatibility/assemble.ts +1 -1
  166. package/src/routing/quota.ts +7 -3
  167. package/src/server/auth-cors.ts +5 -0
  168. package/src/server/chat-completions.ts +6 -1
  169. package/src/server/chat-native.ts +45 -25
  170. package/src/server/claude-messages.ts +16 -2
  171. package/src/server/gui-static.ts +38 -4
  172. package/src/server/index/spend-ledger-lifecycle.ts +10 -4
  173. package/src/server/index.ts +16 -16
  174. package/src/server/inference/context.ts +1 -1
  175. package/src/server/lifecycle.ts +1 -1
  176. package/src/server/management/agent-settings-routes.ts +30 -6
  177. package/src/server/management/anthropic-account-threshold.ts +12 -9
  178. package/src/server/management/anthropic-pool-settings.ts +72 -0
  179. package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
  180. package/src/server/management/config-routes.ts +27 -7
  181. package/src/server/management/decision-routes.ts +2 -1
  182. package/src/server/management/integration-routes.ts +25 -2
  183. package/src/server/management/oauth-account-routes.ts +79 -98
  184. package/src/server/management/provider-instance-ownership.ts +44 -0
  185. package/src/server/management/provider-routes.ts +21 -28
  186. package/src/server/management/route-registry.ts +3 -1
  187. package/src/server/management/routing-profile-routes.ts +4 -4
  188. package/src/server/management/system-restart.ts +44 -14
  189. package/src/server/management/system-routes.ts +24 -2
  190. package/src/server/management/vision-sidecar-options.ts +9 -6
  191. package/src/server/management/web-search-sidecar-options.ts +51 -2
  192. package/src/server/management-auth.ts +6 -0
  193. package/src/server/messages-native-eligibility.ts +3 -2
  194. package/src/server/messages-native-oauth.ts +86 -41
  195. package/src/server/messages-native-scope.ts +4 -1
  196. package/src/server/messages-native-selector.ts +39 -0
  197. package/src/server/messages-native.ts +84 -25
  198. package/src/server/request-log.ts +5 -0
  199. package/src/server/responses/adapter-continuation.ts +29 -17
  200. package/src/server/responses/adapter-dispatch.ts +47 -17
  201. package/src/server/responses/antigravity-validation-refusal.ts +20 -6
  202. package/src/server/responses/combo-requested-effort.ts +14 -0
  203. package/src/server/responses/compact.ts +25 -8
  204. package/src/server/responses/compaction-recovery.ts +1 -1
  205. package/src/server/responses/core-combo-native.ts +4 -1
  206. package/src/server/responses/core-combo.ts +21 -18
  207. package/src/server/responses/core-normalize.ts +1 -0
  208. package/src/server/responses/core-options.ts +3 -1
  209. package/src/server/responses/core-replay.ts +5 -0
  210. package/src/server/responses/passthrough-delivery.ts +7 -3
  211. package/src/server/responses/passthrough-dispatch.ts +21 -8
  212. package/src/server/responses/request-prepare.ts +37 -5
  213. package/src/server/responses/request-send-budget.ts +74 -37
  214. package/src/server/responses/request-spend.ts +325 -33
  215. package/src/server/responses/request-transport.ts +132 -52
  216. package/src/server/responses/run-turn-execution.ts +11 -3
  217. package/src/server/responses/sidecar-execution.ts +96 -14
  218. package/src/server/responses-undeclared-tool-guard.ts +5 -1
  219. package/src/server/workflow-refusal.ts +30 -0
  220. package/src/service/desktop-startup.ts +100 -3
  221. package/src/service/ownership-mutation-lease.d.mts +1 -1
  222. package/src/service/ownership-mutation-lease.mjs +5 -7
  223. package/src/sidecar/anthropic-binding.ts +79 -0
  224. package/src/sidecar/auth.ts +76 -2
  225. package/src/types/anthropic-account-pool.ts +33 -0
  226. package/src/types/config.ts +12 -28
  227. package/src/types/provider.ts +5 -0
  228. package/src/types/tools.ts +7 -0
  229. package/src/types.ts +4 -0
  230. package/src/usage/cost.ts +3 -2
  231. package/src/usage/expected-prices.ts +66 -14
  232. package/src/vision/anthropic-describe.ts +16 -3
  233. package/src/vision/index.ts +3 -0
  234. package/src/vision/plan.ts +19 -6
  235. package/src/web-search/alpha-search.ts +7 -1
  236. package/src/web-search/anthropic-executor.ts +16 -4
  237. package/src/web-search/header-deadline.ts +67 -0
  238. package/src/web-search/index.ts +7 -3
  239. package/src/web-search/loop.ts +32 -10
  240. package/src/web-search/passthrough-bridge.ts +4 -1
  241. package/src/web-search/sidecar-providers.ts +4 -3
  242. package/gui/dist/assets/App-BOUa-ek3.js +0 -54
  243. package/gui/dist/assets/Tray-DUtzB4zF.js +0 -1
  244. package/gui/dist/assets/index-DwHNDF4s.js +0 -94
  245. package/gui/dist/assets/quota-summary-BuL4WIBo.js +0 -1
@@ -1,3 +1,5 @@
1
+ import { hasOrdinaryNativeOpenAiRow } from "./control-plane";
2
+ import { isCodexControlPlaneModel } from "../control-plane-models";
1
3
  import { execFileSync } from "node:child_process";
2
4
  import { createHash } from "node:crypto";
3
5
  import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
@@ -385,7 +387,7 @@ export function clampEntryToCodexSupportedEfforts(
385
387
  entry: RawEntry,
386
388
  supported: ReadonlySet<string> | null,
387
389
  ): void {
388
- if (!supported) return;
390
+ if (!supported || isCodexControlPlaneModel(entry.slug)) return;
389
391
  const levels = Array.isArray(entry.supported_reasoning_levels)
390
392
  ? entry.supported_reasoning_levels as Array<{ effort?: string }>
391
393
  : null;
@@ -532,6 +534,14 @@ export function clampCatalogModelsToObservedCodexSupport(
532
534
  else index += 1;
533
535
  }
534
536
 
537
+ // Only Reserve rows are currently omitted above; ordinary natives keep a fallback ladder.
538
+ // Also repair a persisted orphan so a removed Reserve can never leave a reviewer behind.
539
+ if (!hasOrdinaryNativeOpenAiRow(models)) {
540
+ for (let index = models.length - 1; index >= 0; index -= 1) {
541
+ if (isCodexControlPlaneModel(models[index]!.slug)) models.splice(index, 1);
542
+ }
543
+ }
544
+
535
545
  return {
536
546
  removedEfforts: [...removed].sort(),
537
547
  affectedModels: affected,
@@ -32,6 +32,7 @@ import {
32
32
  type OAuthActiveTokenObservation,
33
33
  } from "../../oauth";
34
34
  import { getAccountSet } from "../../oauth/store";
35
+ import { mayResolveModelsOAuth } from "../../oauth/model-discovery-auth";
35
36
  import type { OcxConfig, OcxProviderConfig } from "../../types";
36
37
  import { modelInList } from "../../types";
37
38
  import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
@@ -389,15 +390,15 @@ export function captureProviderGather(
389
390
  ? authResolver.resolve(name, provider)
390
391
  : undefined;
391
392
  // A refreshing capture carries the stored origin so accounts on different hosts keep separate
392
- // flights. The send is rebuilt from the auth the gather resolves, and the observed path never
393
+ // flights. Host-scoped auth rebuilds its request; B retains its configured captured target. Observed capture never
393
394
  // reads the live store.
394
395
  const oauthApiBaseUrl = observedAuth
395
396
  ? observedAuth.oauthApiBaseUrl
396
- : authResolver.kind === "refreshing" && provider.authMode === "oauth"
397
+ : authResolver.kind === "refreshing" && provider.authMode === "oauth" && mayResolveModelsOAuth(name, provider)
397
398
  ? getOAuthCredentialApiBaseUrl(name)
398
399
  : undefined;
399
400
  const request = captureModelsRequest(name, provider, oauthApiBaseUrl);
400
- const refreshingOAuthAccountId = !observedAuth && authResolver.kind === "refreshing" && provider.authMode === "oauth"
401
+ const refreshingOAuthAccountId = !observedAuth && authResolver.kind === "refreshing" && provider.authMode === "oauth" && mayResolveModelsOAuth(name, provider)
401
402
  ? getAccountSet(name)?.activeAccountId
402
403
  : undefined;
403
404
  const resolved = resolveProviderModelDiscovery(name, provider);
@@ -531,6 +532,7 @@ function providerCatalogFingerprint(name: string, prov: OcxProviderConfig): Reco
531
532
  live: prov.liveModels ?? null,
532
533
  base: prov.baseUrl ?? "",
533
534
  adapter: prov.adapter ?? "",
535
+ instance: prov.anthropicOAuthInstance ?? null,
534
536
  models: [...(prov.models ?? [])].sort(),
535
537
  retain: [...(prov.retainModels ?? [])].sort(),
536
538
  selected: [...(prov.selectedModels ?? [])].sort(),
@@ -1,3 +1,4 @@
1
+ import { isCodexControlPlaneModel } from "../control-plane-models";
1
2
  import { execFileSync } from "node:child_process";
2
3
  import { createHash } from "node:crypto";
3
4
  import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
@@ -700,7 +701,7 @@ export function catalogEntryIsNativeChatGpt(entry: RawEntry): boolean {
700
701
  && entry.use_responses_lite === true
701
702
  && hasNativeOpenAiCapabilityMetadata(routedNativeSlug)
702
703
  ) return true;
703
- if (UPSTREAM_NATIVE_ENTRIES.has(slug) || SUPPORTED_NATIVE_OPENAI_SLUGS.has(slug)) return true;
704
+ if (isCodexControlPlaneModel(slug) || UPSTREAM_NATIVE_ENTRIES.has(slug) || SUPPORTED_NATIVE_OPENAI_SLUGS.has(slug)) return true;
704
705
  return false;
705
706
  }
706
707
 
@@ -1,3 +1,4 @@
1
+ import { isAnthropicOAuthInstance } from "../../providers/anthropic-instance";
1
2
  import { effectiveProviderAlias, effectiveProviderAliasDecision } from "../../providers/default-aliases";
2
3
  import { initialModelSelectionPending } from "../../providers/initial-model-selection";
3
4
  import { execFileSync } from "node:child_process";
@@ -28,11 +29,12 @@ import {
28
29
  } from "../model-cache";
29
30
  import {
30
31
  buildModelsRequest,
31
- getValidAccessTokenSnapshot,
32
+ getModelsOAuthAccessSnapshot,
32
33
  observeActiveOAuthAccessToken,
33
34
  resolveModelsAuthToken,
34
35
  type OAuthActiveTokenObservation,
35
36
  } from "../../oauth";
37
+ import { mayResolveModelsOAuth } from "../../oauth/model-discovery-auth";
36
38
  import type { OcxConfig, OcxProviderConfig } from "../../types";
37
39
  import { modelInList } from "../../types";
38
40
  import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
@@ -145,7 +147,8 @@ export function observedModelsAuthResolver(
145
147
  return { apiKey: resolveProviderApiKey(provider.apiKey), observed: true };
146
148
  }
147
149
 
148
- const observation = observeActiveOAuthAccessToken(name, authStoreBuffer);
150
+ const observation: OAuthActiveTokenObservation = mayResolveModelsOAuth(name, provider)
151
+ ? observeActiveOAuthAccessToken(name, authStoreBuffer) : { kind: "missing" };
149
152
  outcomes.push({ provider: name, state: observation.kind });
150
153
  if (observation.kind !== "available") return { apiKey: undefined, observed: true };
151
154
  return {
@@ -179,7 +182,7 @@ export async function fetchProviderModelsWithAuth(
179
182
  // generation, so a request started with the former account cannot later publish its result.
180
183
  const cacheGeneration = captureModelCacheGeneration(name);
181
184
  const isCurrentCacheGeneration = () => isModelCacheGenerationCurrent(name, cacheGeneration);
182
- const anthropicSelection = name === "anthropic" && prov.authMode === "oauth"
185
+ const anthropicSelection = isAnthropicOAuthInstance(name) && prov.authMode === "oauth" && mayResolveModelsOAuth(name, prov)
183
186
  ? captureOAuthAccountSelection(name) : null;
184
187
  if (prov.authMode === "forward") return observed([], "authoritative"); // ChatGPT backend has no /models
185
188
  const seedVertexDefault = prov.adapter === "google"
@@ -257,9 +260,11 @@ export async function fetchProviderModelsWithAuth(
257
260
  }
258
261
  return observed(configured, "authoritative");
259
262
  }
260
- const auth: ModelsAuthResolution = captured.observedAuth ?? (resolveAuth.kind === "refreshing"
263
+ const auth: ModelsAuthResolution = prov.authMode === "oauth" && !mayResolveModelsOAuth(name, prov)
264
+ ? { apiKey: undefined, observed: resolveAuth.kind === "observed" }
265
+ : captured.observedAuth ?? (resolveAuth.kind === "refreshing"
261
266
  ? prov.authMode === "oauth"
262
- ? await getValidAccessTokenSnapshot(name)
267
+ ? await getModelsOAuthAccessSnapshot(name, prov)
263
268
  .then(snapshot => ({
264
269
  apiKey: snapshot.accessToken,
265
270
  observed: false,
@@ -274,7 +279,8 @@ export async function fetchProviderModelsWithAuth(
274
279
  : resolveAuth.resolve(name, prov));
275
280
  const apiKey = auth.apiKey;
276
281
  const maySendAnthropicDiscovery = () => {
277
- if (name !== "anthropic" || prov.authMode !== "oauth") return true;
282
+ if (!isAnthropicOAuthInstance(name) || prov.authMode !== "oauth") return true;
283
+ if (!mayResolveModelsOAuth(name, prov)) return false;
278
284
  const selected = captureOAuthAccountSelection(name);
279
285
  const row = auth.oauthAccountId ? getAccountCredentialWithStatus(name, auth.oauthAccountId) : null;
280
286
  return !!anthropicSelection && !!selected && !!row && !row.paused && !row.needsReauth
@@ -633,7 +639,7 @@ export async function fetchProviderModelsWithAuth(
633
639
  }
634
640
  // The captured request predates any refresh, so a refreshing gather rebuilds it
635
641
  // from the auth it resolved: the token and its origin, together.
636
- const request = resolveAuth.kind === "refreshing"
642
+ const request = resolveAuth.kind === "refreshing" && name !== "anthropic2"
637
643
  ? captureModelsRequest(name, prov, auth.oauthApiBaseUrl)
638
644
  : captured.request;
639
645
  const url = request.url;
@@ -653,7 +659,7 @@ export async function fetchProviderModelsWithAuth(
653
659
  const failedDiscoveryFallback = (
654
660
  failure: ProviderModelDiscoveryFailure,
655
661
  ): { models: CatalogModel[]; fallback: "stale" | "configured"; shouldLog: boolean } => {
656
- if (!isCurrentCacheGeneration()) {
662
+ if (!isCurrentCacheGeneration() || !maySendAnthropicDiscovery()) {
657
663
  return {
658
664
  models: withConfiguredRetention(failedDiscoveryConfigured),
659
665
  fallback: "configured",
@@ -854,7 +860,7 @@ export async function fetchProviderModelsWithAuth(
854
860
  `[opencodex] Provider model discovery for "${name}" returned an authoritative empty catalog; ${droppedConfiguredIds.length > 0 ? `dropping configured model ids: ${droppedConfiguredIds.join(", ")}` : "no models will be exposed"}.`,
855
861
  );
856
862
  }
857
- if (!setCached(name, forCache, Date.now(), cacheGeneration)) {
863
+ if (!maySendAnthropicDiscovery() || !setCached(name, forCache, Date.now(), cacheGeneration)) {
858
864
  return observed(withConfiguredRetention(configured), "degraded");
859
865
  }
860
866
  markProviderDiscoveryOk(name, liveModelCount);
@@ -0,0 +1,6 @@
1
+ /** Exact Codex control-plane ids routed to canonical OpenAI, outside the public native roster. */
2
+ export const CODEX_INTERNAL_OPENAI_MODELS: ReadonlySet<string> = new Set(["codex-auto-review"]);
3
+
4
+ export function isCodexControlPlaneModel(slug: unknown): boolean {
5
+ return typeof slug === "string" && CODEX_INTERNAL_OPENAI_MODELS.has(slug);
6
+ }
@@ -85,6 +85,9 @@ export interface RestoreJournalResult {
85
85
  ownershipRefusal?: CodexHomeOwnerRefusalReason;
86
86
  configRestored: boolean;
87
87
  profileRestored: boolean;
88
+ /** True only when restore rewrote or removed a file (not when state was already original). */
89
+ configRewritten: boolean;
90
+ profileRewritten: boolean;
88
91
  configChanged: boolean;
89
92
  profileChanged: boolean;
90
93
  complete: boolean;
@@ -299,12 +302,13 @@ export function restoreJournalState(): RestoreJournalResult {
299
302
  try { assertCodexHomeOwner(CODEX_HOME); }
300
303
  catch (error) {
301
304
  if (!(error instanceof CodexHomeOwnerRefusal)) throw error;
302
- return { configRestored: false, profileRestored: false, configChanged: false, profileChanged: false,
303
- complete: false, unverified: true, ownershipRefusal: error.reason };
305
+ return { configRestored: false, profileRestored: false, configRewritten: false, profileRewritten: false,
306
+ configChanged: false, profileChanged: false, complete: false, unverified: true, ownershipRefusal: error.reason };
304
307
  }
305
308
  const journal = readJournal();
306
309
  if (!journal) {
307
- return { configRestored: false, profileRestored: false, configChanged: false, profileChanged: false, complete: false, unverified: false };
310
+ return { configRestored: false, profileRestored: false, configRewritten: false, profileRewritten: false,
311
+ configChanged: false, profileChanged: false, complete: false, unverified: false };
308
312
  }
309
313
  const currentConfig = existsSync(CODEX_CONFIG_PATH) ? readFileSync(CODEX_CONFIG_PATH, "utf-8") : null;
310
314
  const currentProfile = existsSync(CODEX_PROFILE_PATH) ? readFileSync(CODEX_PROFILE_PATH, "utf-8") : null;
@@ -316,6 +320,8 @@ export function restoreJournalState(): RestoreJournalResult {
316
320
  return {
317
321
  configRestored: comparison.configAlreadyOriginal,
318
322
  profileRestored: comparison.profileAlreadyOriginal,
323
+ configRewritten: false,
324
+ profileRewritten: false,
319
325
  configChanged: !configUnchanged,
320
326
  profileChanged: !profileUnchanged,
321
327
  complete: false,
@@ -326,14 +332,18 @@ export function restoreJournalState(): RestoreJournalResult {
326
332
  assertCodexHomeOwner(CODEX_HOME);
327
333
  let configRestored = comparison.configAlreadyOriginal;
328
334
  let profileRestored = comparison.profileAlreadyOriginal;
335
+ let configRewritten = false;
336
+ let profileRewritten = false;
329
337
  if (configUnchanged && !configRestored) {
330
338
  atomicWriteFile(CODEX_CONFIG_PATH, comparison.originalConfig);
331
339
  configRestored = true;
340
+ configRewritten = true;
332
341
  }
333
342
  if (profileUnchanged && !profileRestored) {
334
343
  if (comparison.originalProfile !== null) {
335
344
  atomicWriteFile(CODEX_PROFILE_PATH, comparison.originalProfile);
336
345
  profileRestored = true;
346
+ profileRewritten = true;
337
347
  } else if (existsSync(CODEX_PROFILE_PATH)) {
338
348
  // "There was no profile before, so remove the one we generated." Claiming success
339
349
  // without checking is how a caller ends up deleting the journal, reporting a clean
@@ -343,6 +353,7 @@ export function restoreJournalState(): RestoreJournalResult {
343
353
  try {
344
354
  unlinkSync(CODEX_PROFILE_PATH);
345
355
  profileRestored = true;
356
+ profileRewritten = true;
346
357
  } catch (error) {
347
358
  profileRestored = (error as NodeJS.ErrnoException).code === "ENOENT";
348
359
  }
@@ -355,6 +366,8 @@ export function restoreJournalState(): RestoreJournalResult {
355
366
  return {
356
367
  configRestored,
357
368
  profileRestored,
369
+ configRewritten,
370
+ profileRewritten,
358
371
  configChanged: !configUnchanged,
359
372
  profileChanged: !profileUnchanged,
360
373
  complete,
@@ -367,6 +380,11 @@ export function restoreJournal(): boolean {
367
380
  return restoreJournalState().complete;
368
381
  }
369
382
 
383
+ /** A no-rewrite recovery is quiet only when the stale journal is actually gone. */
384
+ function warnIfJournalRetained(): void {
385
+ if (existsSync(JOURNAL_PATH)) console.error("⚠️ Codex journal recovery found nothing to restore, but the journal could not be removed; it will be retried on the next start.");
386
+ }
387
+
370
388
  export interface ReconcileJournalOptions {
371
389
  activeClientApiKeyId?: string;
372
390
  }
@@ -388,7 +406,13 @@ export function reconcileJournal(options: ReconcileJournalOptions = {}): boolean
388
406
  console.error("⚠️ Codex journal recovery was not verified; current configuration files and the journal were preserved.");
389
407
  return false;
390
408
  }
391
- if (!restored.configRestored && !restored.profileRestored) return false;
409
+ if (!restored.complete) {
410
+ console.error("⚠️ Codex journal recovery was incomplete; the journal was preserved — check the Codex files.");
411
+ return false;
412
+ }
413
+ // Warn only when restore actually rewrote state; an already-original
414
+ // snapshot still cleans itself up but has nothing to report.
415
+ if (!restored.configRewritten && !restored.profileRewritten) { warnIfJournalRetained(); return false; }
392
416
  console.error(`⚠️ Uncommitted or mismatched client routing (${owner.apiKeyId}) was restored from the Codex journal.`);
393
417
  return true;
394
418
  }
@@ -406,7 +430,14 @@ export function reconcileJournal(options: ReconcileJournalOptions = {}): boolean
406
430
  console.error("⚠️ Codex journal recovery was not verified; current configuration files and the journal were preserved.");
407
431
  return false;
408
432
  }
409
- if (!restored.configRestored && !restored.profileRestored) return false;
433
+ if (!restored.complete) {
434
+ console.error("⚠️ Codex journal recovery was incomplete; the journal was preserved — check the Codex files.");
435
+ return false;
436
+ }
437
+ // Warn only when restore actually rewrote state. A stale journal whose
438
+ // artifacts are already original still cleans itself up (complete removes
439
+ // it) but has nothing to report — claiming a restore then cries wolf.
440
+ if (!restored.configRewritten && !restored.profileRewritten) { warnIfJournalRetained(); return false; }
410
441
  console.error(`⚠️ Previous session (PID ${pid}) did not shut down cleanly. Codex state restored from journal.`);
411
442
  return true;
412
443
  }
@@ -48,11 +48,19 @@ function lexical(path: string): Stats | null {
48
48
  try { return lstatSync(path); }
49
49
  catch (error) { if (fileErrorCode(error) === "ENOENT") return null; throw error; }
50
50
  }
51
+ /** Name the path and the failed check; a group/world-writable owned file gets the chmod that fixes it. */
52
+ function untrustedArtifact(path: string, stat: Stats): Error {
53
+ const prefix = `Private Codex artifact is not an owned regular file: ${path}`;
54
+ if (stat.isSymbolicLink() || !stat.isFile()) return new Error(`${prefix} is not a regular file`);
55
+ if (stat.uid !== process.getuid?.()) return new Error(`${prefix} is owned by uid ${stat.uid}`);
56
+ const mode = (stat.mode & 0o777).toString(8).padStart(4, "0");
57
+ return new Error(`${prefix} is group- or world-writable (mode ${mode}); run chmod 600 ${shQuote(path)} and retry`);
58
+ }
51
59
  function snapshot(path: string): Snapshot | null {
52
60
  const before = lexical(path);
53
61
  if (!before) return null;
54
62
  if (!before.isFile() || before.isSymbolicLink() || before.uid !== process.getuid?.() || (before.mode & 0o022)) {
55
- throw new Error("Private Codex artifact is not an owned regular file");
63
+ throw untrustedArtifact(path, before);
56
64
  }
57
65
  const bounded = readBoundedRegularFile(path, PRIVATE_FILE_MAX_BYTES);
58
66
  if (!bounded || "warning" in bounded) throw new Error("Private Codex artifact is unreadable or changed");
@@ -10,11 +10,34 @@ export const JEV_DECISION_TIMEOUT_MAX_MS = 120_000;
10
10
  /** Decision deadline when a combo sets no `decisionTimeoutMs`. */
11
11
  export const JEV_DECISION_TIMEOUT_DEFAULT_MS = 4_000;
12
12
 
13
- /** Whether a self-hosted decision endpoint follows the documented Jev `/systemone` path. */
13
+ /** HTTPS decision services may use any path; local cleartext keeps the `/systemone` contract. */
14
14
  export function isSystemOneEndpoint(baseUrl: string): boolean {
15
15
  try {
16
- return new URL(baseUrl.trim()).pathname.replace(/\/+$/, "").endsWith("/systemone");
16
+ const raw = baseUrl.trim();
17
+ // URL drops empty delimiters ("?", "#", "@") and strips tab/CR/LF, so check the raw text first.
18
+ if (/[\u0000-\u001f\u007f]/.test(raw)) return false;
19
+ const authority = raw.replace(/^[a-z][a-z\d+.-]*:[/\\]*/i, "").split(/[/\\]/, 1)[0] ?? "";
20
+ if (/[?#]/.test(raw) || authority.includes("@")) return false;
21
+ const url = new URL(raw);
22
+ if (url.username || url.password || url.search || url.hash) return false;
23
+ // URL canonicalizes address literals; keep this import-free for the dashboard bundle.
24
+ const host = url.hostname;
25
+ const local = host === "localhost" || host === "[::1]"
26
+ || /^(?:(?:127|10)\.\d+|172\.(?:1[6-9]|2\d|3[01])|192\.168)\.\d+\.\d+$/.test(host)
27
+ || /^\[f[cd][\da-f]{2}:/.test(host) || /^\[::ffff:7f[\da-f]{2}:/.test(host);
28
+ return url.protocol === "https:"
29
+ || url.protocol === "http:" && local && url.pathname.replace(/\/+$/, "").endsWith("/systemone");
17
30
  } catch {
18
31
  return false;
19
32
  }
20
33
  }
34
+
35
+ /**
36
+ * The URL a decision row is sent to. A `/systemone` path keeps its historical trailing-slash
37
+ * normalization; any other HTTPS path is the operator's exact endpoint.
38
+ */
39
+ export function jevDecisionEndpointUrl(baseUrl: string): string {
40
+ const raw = baseUrl.trim();
41
+ const stripped = raw.replace(/\/+$/, "");
42
+ return stripped.endsWith("/systemone") ? stripped : raw;
43
+ }
package/src/combos/jev.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  } from "../providers/api-key-resolve";
11
11
  import { providerMatchesRegistryTransport } from "../providers/registry";
12
12
  import type { OcxComboDefaultEffort, OcxConfig, OcxProviderConfig } from "../types";
13
+ import { jevDecisionEndpointUrl } from "./jev-decision-contract";
13
14
  import {
14
15
  isSystemOneEndpoint,
15
16
  JEV_DECISION_TIMEOUT_DEFAULT_MS,
@@ -47,6 +48,7 @@ const TOOL_NAME_CHARS = 160;
47
48
  const VISIBLE_TEXT_CHUNK_CHARS = 16_384;
48
49
 
49
50
  const ENVELOPE_TAGS = [
51
+ "system-reminder",
50
52
  "codex_internal_context",
51
53
  "recommended_plugins",
52
54
  "environment_context",
@@ -302,7 +304,8 @@ function taskWithoutProtectedEnvelopes(text: string): string {
302
304
  }
303
305
  } else {
304
306
  if (stack.length === 0) appendTrimmedRange(visible, "\n", 0, 1);
305
- if (!completedGoal && !goal && tag === "codex_internal_context") {
307
+ // A Codex goal quoted inside a Claude reminder is context, never a fallback task.
308
+ if (!completedGoal && !goal && tag === "codex_internal_context" && !stack.includes("system-reminder")) {
306
309
  goal = { sample: emptyTextSample(), pendingWhitespace: emptyTextSample() };
307
310
  goalDepth = stack.length;
308
311
  }
@@ -674,7 +677,7 @@ function selfHostedApiKey(name: string, apiKey: string | undefined): string | un
674
677
  * only while the row still matches the registry transport, and the environment fallbacks exist
675
678
  * only for that URL. A retargeted `jev` row therefore keeps today's behavior instead of becoming a
676
679
  * custom destination. Any other id must be an enabled `jev-decision` row whose baseUrl is a
677
- * `/systemone` endpoint and which names its own model; only its own key may accompany it, so no
680
+ * full HTTPS decision endpoint (or a local HTTP `/systemone` endpoint) and names its own model; only its own key may accompany it, so no
678
681
  * TypeSafe credential can reach a self-hosted service. `undefined` means no usable decision
679
682
  * service (reported through the existing `missing_key` gate); `null` means the request's
680
683
  * destination scope refused it before any credential access.
@@ -707,7 +710,7 @@ function jevDecisionEndpoint(
707
710
  };
708
711
  }
709
712
  if (configured?.adapter !== "jev-decision" || typeof configured.baseUrl !== "string") return undefined;
710
- const url = configured.baseUrl.trim().replace(/\/+$/, "");
713
+ const url = jevDecisionEndpointUrl(configured.baseUrl);
711
714
  if (!url || !isSystemOneEndpoint(url)) return undefined;
712
715
  // `jev-latest` is TypeSafe's model name; a self-hosted host must name its own.
713
716
  const model = configured.defaultModel?.trim() || configured.models?.[0]?.trim();
@@ -302,7 +302,7 @@ export function comboConfigIssues(
302
302
  } else if (!isSystemOneEndpoint(String(providers[decisionProvider]?.baseUrl ?? ""))) {
303
303
  issues.push({
304
304
  path: ["decisionProvider"],
305
- message: `decisionProvider "${decisionProvider}" baseUrl must be the full decision endpoint ending in /systemone`,
305
+ message: `decisionProvider "${decisionProvider}" baseUrl must be a full HTTPS decision endpoint or an HTTP /systemone endpoint`,
306
306
  });
307
307
  } else if (options.requireUsableDecisionService && providers[decisionProvider]?.disabled === true) {
308
308
  issues.push({
@@ -1,3 +1,4 @@
1
+ import { readExistingSpendSalt, spendPoolAliasesError, validatePoolAliasOwners } from "../lib/spend-pool-alias-validation";
1
2
  import { isSubagentModelEntry, rawSubagentModelForce } from "./subagent-models";
2
3
  import { protocolConfigSchema } from "./schema/config-schema";
3
4
  import { createHash } from "node:crypto";
@@ -7,13 +8,13 @@ import * as z from "zod/v4";
7
8
  import { compactionRecoveryConfigError } from "./schema/compaction-recovery";
8
9
  import { blockedModelRedirectsError } from "./schema/blocked-model-redirects";
9
10
  import type { OcxConfig } from "../types";
10
- import { parseAnthropicModelRoutes } from "../oauth/anthropic-model-routes";
11
+ import { anthropicAccountPoolConfigError, anthropicOAuthInstanceConfigError, anthropicSidecarConfigError } from "./schema/anthropic-account-pool";
11
12
  import { configReasoningPinsConfigError } from "./provider-validation";
12
13
  import { loopbackCompanionAllowed } from "../codex/loopback-target";
13
14
  import { UPSTREAM_HOST_CIRCUIT_MAX_THRESHOLD } from "../codex/upstream-host-health";
14
15
  import { MAX_APP_OWNED_MEMORY_BUDGET_MB, MIN_APP_OWNED_MEMORY_BUDGET_MB } from "../lib/app-owned-memory";
15
16
  import { isMissingPathError } from "./atomic-write";
16
- import { getConfigPath } from "./paths";
17
+ import { getConfigDir, getConfigPath } from "./paths";
17
18
  import { getDefaultConfig } from "./proxy-env";
18
19
  import { salvageConfigCandidate } from "./salvage";
19
20
  import {
@@ -81,6 +82,23 @@ export type ConfigDiagnostics = {
81
82
  warnings?: string[];
82
83
  };
83
84
 
85
+ function rawPoolAliasesError(value: unknown): string | undefined {
86
+ const record = rawConfigRecord(value);
87
+ if (!record) return undefined;
88
+ const aliases = record.spendPoolAliases;
89
+ const shapeError = spendPoolAliasesError(aliases);
90
+ if (shapeError) return shapeError;
91
+ const mapping = rawConfigRecord(aliases);
92
+ if (!mapping || Object.keys(mapping).length === 0) return undefined;
93
+ const providers = rawConfigRecord(record.providers ?? {});
94
+ if (!providers) return "providers must be an object to validate spendPoolAliases";
95
+ const targetError = spendPoolAliasesError(aliases, Object.keys(providers));
96
+ if (targetError) return targetError;
97
+ const salt = readExistingSpendSalt(getConfigDir());
98
+ if (!salt) return "spendPoolAliases requires a safely readable existing spend salt";
99
+ return validatePoolAliasOwners(aliases, salt, Object.keys(providers));
100
+ }
101
+
84
102
  export type ConfigFileSnapshot = {
85
103
  diagnostics: ConfigDiagnostics;
86
104
  /** Exact file contents, including a possible BOM, used as the optimistic revision. */
@@ -145,6 +163,9 @@ function validFileConfigDiagnostics(config: OcxConfig, rawParsed: unknown): Conf
145
163
  if (codexPoolWarning) warnings.push(codexPoolWarning);
146
164
  const spendWarning = malformedSpendWarning(rawParsed);
147
165
  if (spendWarning) warnings.push(spendWarning);
166
+ if (rawPoolAliasesError(rawParsed)) {
167
+ warnings.push("spendPoolAliases invalid: configured pool admission is refused until the mapping is corrected");
168
+ }
148
169
  const plaintextWarning = malformedPlaintextV2AgentMessagesWarning(rawParsed);
149
170
  if (plaintextWarning) warnings.push(plaintextWarning);
150
171
  if (syncDisabledReason) {
@@ -153,7 +174,7 @@ function validFileConfigDiagnostics(config: OcxConfig, rawParsed: unknown): Conf
153
174
  return {
154
175
  config: normalized,
155
176
  source: "file",
156
- error: null,
177
+ error: anthropicOAuthInstanceConfigError(rawParsed) ?? anthropicAccountPoolConfigError(rawParsed) ?? anthropicSidecarConfigError(rawParsed) ?? null,
157
178
  ...(warnings.length > 0 ? { warnings } : {}),
158
179
  };
159
180
  }
@@ -655,19 +676,8 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
655
676
  if (protocols !== undefined && !protocolConfigSchema.safeParse(protocols).success) {
656
677
  return { ok: false, error: "schema_invalid: protocols: expected valid protocol policy and boolean rollout flags" };
657
678
  }
658
- const rawAnthropicPool = rawConfigRecord(value)?.anthropicAccountPool;
659
- const anthropicPool = rawConfigRecord(rawAnthropicPool);
660
- if (rawAnthropicPool !== undefined && !anthropicPool) {
661
- return { ok: false, error: "schema_invalid: anthropicAccountPool: must be an object" };
662
- }
663
- if (anthropicPool && Object.hasOwn(anthropicPool, "nativeMessages") && typeof anthropicPool.nativeMessages !== "boolean") {
664
- return { ok: false, error: "schema_invalid: anthropicAccountPool.nativeMessages: must be a boolean" };
665
- }
666
- const routeValue = anthropicPool?.routes;
667
- if (routeValue !== undefined) {
668
- const parsed = parseAnthropicModelRoutes(routeValue);
669
- if (!parsed.ok) return { ok: false, error: `schema_invalid: anthropicAccountPool.routes: ${parsed.error}` };
670
- }
679
+ const anthropicError = anthropicOAuthInstanceConfigError(value) ?? anthropicAccountPoolConfigError(value) ?? anthropicSidecarConfigError(value);
680
+ if (anthropicError) return { ok: false, error: anthropicError };
671
681
  const boundaryError = blockedModelRedirectsError(value)
672
682
  ?? compactionRecoveryConfigError(value) ?? configReasoningPinsConfigError(value)
673
683
  ?? blankHostnameError(value)
@@ -679,6 +689,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
679
689
  ?? agentTaskRecoveryError(value)
680
690
  ?? quotaResetNotifyError(value)
681
691
  ?? catalogAutoRefreshError(value)
692
+ ?? (rawPoolAliasesError(value) ? "schema_invalid: spendPoolAliases: invalid pool identity mapping" : null)
682
693
  ?? spendError(value)
683
694
  ?? codexPoolError(value)
684
695
  ?? googleAntigravityStaticCatalogVersionError(value)
@@ -3,6 +3,7 @@ import { chmodSync, existsSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { compactionRecoveryConfigError } from "./schema/compaction-recovery";
5
5
  import { blockedModelRedirectsError } from "./schema/blocked-model-redirects";
6
+ import { anthropicSidecarConfigError } from "./schema/anthropic-account-pool";
6
7
  import {
7
8
  modelPinnedEffortsConfigError,
8
9
  pinnedReasoningEffortConfigError,
@@ -160,6 +161,39 @@ export function warnDegradedMemoryModels(rawParsed: unknown, validated: OcxConfi
160
161
  }
161
162
  }
162
163
 
164
+ /**
165
+ * Load-time degradation for helper `anthropicInstance` (loadConfig only). A malformed id, or one
166
+ * that conflicts with its block's backend or model, drops just that key with a warning, so the
167
+ * helper inherits the request's pool and every provider/key survives. Validated writes still
168
+ * reject the value and file diagnostics still report it from the raw document.
169
+ *
170
+ * Each location is checked in isolation, global blocks first, so a Claude override is judged
171
+ * against the already-degraded global block it inherits from. An override that owns no
172
+ * `anthropicInstance` of its own is left untouched: the conflicting key lives in the global
173
+ * block, which is valid on its own, and the inherited value is still a well-formed instance.
174
+ */
175
+ export function sanitizeAnthropicSidecarInstanceForLoad(parsed: unknown): void {
176
+ const asRecord = (value: unknown): Record<string, unknown> | undefined =>
177
+ value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
178
+ const root = asRecord(parsed);
179
+ if (!root) return;
180
+ for (const scope of ["global", "claude"] as const) {
181
+ const owner = scope === "global" ? root : asRecord(root.claudeCode);
182
+ for (const field of ["webSearchSidecar", "visionSidecar"] as const) {
183
+ const sidecar = asRecord(owner?.[field]);
184
+ if (!sidecar || !Object.hasOwn(sidecar, "anthropicInstance")) continue;
185
+ const probe = scope === "global"
186
+ ? { [field]: sidecar }
187
+ : { [field]: root[field], claudeCode: { [field]: sidecar } };
188
+ const error = anthropicSidecarConfigError(probe);
189
+ if (!error) continue;
190
+ delete sidecar.anthropicInstance;
191
+ // The message names a static path and reason only; the hand-edited value is never echoed.
192
+ console.warn(`⚠️ config.json ${error} — ignoring that anthropicInstance; the helper inherits the request's Anthropic pool`);
193
+ }
194
+ }
195
+ }
196
+
163
197
  /**
164
198
  * Load-time degradation for `retryOn429` (loadConfig only): one hand-edited invalid optional
165
199
  * field (e.g. `attempts: 0` or a string) must not trip the whole provider schema and hide every