@bitkyc08/opencodex 2.77.0 → 2.78.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.
- package/bin/ocx.mjs +11 -10
- package/gui/dist/assets/App-BtfG5id-.js +54 -0
- package/gui/dist/assets/App-lCIslZ1L.css +1 -0
- package/gui/dist/assets/{Tray-gloIYNaW.js → Tray-DiJpSub0.js} +1 -1
- package/gui/dist/assets/index-Btc3Ogw4.js +94 -0
- package/gui/dist/assets/index-DmQB8D_R.css +1 -0
- package/gui/dist/assets/quota-summary-B_a96nXj.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +19 -2
- package/src/adapters/anthropic/client-identity.ts +13 -4
- package/src/adapters/anthropic/native-client-preamble.ts +31 -0
- package/src/adapters/anthropic/passthrough.ts +103 -25
- package/src/adapters/devin/cloud-direct/chat.ts +7 -2
- package/src/adapters/devin/trajectory.ts +49 -0
- package/src/adapters/devin.ts +13 -0
- package/src/adapters/ollama-native.ts +35 -19
- package/src/claude/intercept/client-hello.ts +103 -0
- package/src/claude/intercept/model-bindings.ts +3 -2
- package/src/claude/intercept/picker-listener.ts +279 -66
- package/src/cli/access-audio-input.ts +84 -0
- package/src/cli/access-audio-live.ts +99 -0
- package/src/cli/access-audio.ts +123 -0
- package/src/cli/access-data-client.ts +127 -0
- package/src/cli/access-data-plane.ts +104 -0
- package/src/cli/access-data-response.ts +76 -0
- package/src/cli/access.ts +65 -2
- package/src/cli/account-api.ts +30 -6
- package/src/cli/account-auth.ts +111 -42
- package/src/cli/account-key-quota.ts +118 -0
- package/src/cli/account-main.ts +11 -4
- package/src/cli/account-next-actions.ts +63 -0
- package/src/cli/account-policy-dto.ts +68 -0
- package/src/cli/account-policy.ts +167 -0
- package/src/cli/account-target.ts +8 -0
- package/src/cli/account.ts +34 -8
- package/src/cli/agent-runtime-settings.ts +130 -0
- package/src/cli/agent-settings.ts +101 -0
- package/src/cli/agent.ts +19 -2
- package/src/cli/alias.ts +7 -2
- package/src/cli/aside-profiles.ts +2 -2
- package/src/cli/capabilities-access-remote.ts +147 -0
- package/src/cli/capabilities-accounts.ts +195 -0
- package/src/cli/capabilities-agents-routing.ts +414 -0
- package/src/cli/capabilities-base.ts +783 -0
- package/src/cli/capabilities-command.ts +1 -0
- package/src/cli/capabilities-integrations.ts +264 -0
- package/src/cli/capabilities-lab.ts +214 -0
- package/src/cli/capabilities-observe-system.ts +662 -0
- package/src/cli/capabilities-provider-models.ts +302 -0
- package/src/cli/capabilities.ts +30 -1199
- package/src/cli/capability-types.ts +54 -0
- package/src/cli/catalog-command-result.ts +57 -0
- package/src/cli/chatgpt-command.ts +25 -2
- package/src/cli/claude-agent-startup-sync.ts +17 -0
- package/src/cli/claude-desktop-profile.ts +100 -0
- package/src/cli/claude-desktop.ts +39 -6
- package/src/cli/codex-shim-autorestore.ts +2 -1
- package/src/cli/codex-shim-readiness.ts +23 -3
- package/src/cli/combo-input.ts +88 -0
- package/src/cli/combo-result.ts +52 -0
- package/src/cli/combo-stats.ts +123 -0
- package/src/cli/combo.ts +37 -14
- package/src/cli/companion-timeline.ts +142 -0
- package/src/cli/companion-usage.ts +146 -0
- package/src/cli/companion.ts +12 -3
- package/src/cli/config-command.ts +24 -8
- package/src/cli/dispatch.ts +52 -79
- package/src/cli/doctor.ts +22 -4
- package/src/cli/help-catalog.ts +31 -14
- package/src/cli/help-recovery.ts +2 -2
- package/src/cli/help.ts +25 -11
- package/src/cli/index.ts +38 -27
- package/src/cli/injection-follow.ts +35 -0
- package/src/cli/inspect.ts +5 -0
- package/src/cli/integration-aside-sync.ts +69 -0
- package/src/cli/integration-cursor.ts +106 -0
- package/src/cli/integration-input.ts +60 -0
- package/src/cli/integration-journal.ts +49 -0
- package/src/cli/integration-plan-dto.ts +156 -0
- package/src/cli/integration-preview.ts +215 -0
- package/src/cli/integrations.ts +24 -18
- package/src/cli/json-input.ts +134 -0
- package/src/cli/link.ts +32 -4
- package/src/cli/local-sync-result.ts +41 -0
- package/src/cli/log-follow.ts +84 -0
- package/src/cli/log-view-filter.ts +146 -0
- package/src/cli/logout-command.ts +44 -0
- package/src/cli/model-picker-ordering.ts +137 -0
- package/src/cli/models-custom-input.ts +107 -0
- package/src/cli/models-custom-runtime.ts +112 -0
- package/src/cli/models-order.ts +194 -0
- package/src/cli/models-runtime-subcommands.ts +2 -0
- package/src/cli/models-runtime.ts +7 -0
- package/src/cli/models.ts +66 -81
- package/src/cli/observe-stream.ts +142 -0
- package/src/cli/observe.ts +75 -34
- package/src/cli/provider-batch.ts +77 -0
- package/src/cli/provider-lifecycle-runtime.ts +170 -0
- package/src/cli/provider-result.ts +124 -0
- package/src/cli/provider-runtime.ts +15 -12
- package/src/cli/provider-settings.ts +143 -0
- package/src/cli/provider.ts +112 -34
- package/src/cli/registry.ts +62 -20
- package/src/cli/remote-workspace-hub.ts +83 -0
- package/src/cli/remote-workspace.ts +6 -1
- package/src/cli/restart-failure.ts +27 -0
- package/src/cli/root.ts +12 -0
- package/src/cli/route-policy-write.ts +137 -0
- package/src/cli/route-policy.ts +8 -0
- package/src/cli/runtime-api.ts +56 -79
- package/src/cli/secret-args.ts +149 -0
- package/src/cli/settings-result.ts +30 -0
- package/src/cli/storage.ts +77 -7
- package/src/cli/system-command.ts +60 -2
- package/src/cli/system-restart-client.ts +20 -3
- package/src/cli/system-settings-parity.ts +83 -0
- package/src/cli/update-restart-candidate.ts +21 -0
- package/src/cli/update-restart-child.ts +81 -0
- package/src/cli/update-restart-home.ts +38 -0
- package/src/cli/update-restart-transport.ts +193 -0
- package/src/cli/update-restart.ts +260 -0
- package/src/cli/usage-model-search.ts +58 -0
- package/src/cli/usage-report.ts +13 -5
- package/src/cli/v2-input.ts +66 -0
- package/src/cli/v2-local-output.ts +37 -0
- package/src/cli/v2-runtime.ts +90 -0
- package/src/cli/v2.ts +102 -144
- package/src/clients/config-export/dsh.ts +21 -0
- package/src/clients/config-export.ts +7 -1
- package/src/codex/catalog/build-entries.ts +1 -1
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/heal-observation.ts +66 -0
- package/src/codex/catalog/publication-observer.ts +19 -0
- package/src/codex/catalog/remote.ts +13 -15
- package/src/codex/catalog/restore.ts +19 -6
- package/src/codex/catalog/retained-sync.ts +58 -10
- package/src/codex/catalog/routed-removal.ts +116 -0
- package/src/codex/catalog/write-audit-contract.ts +21 -0
- package/src/codex/catalog/write-audit.ts +171 -0
- package/src/codex/catalog-self-heal.ts +284 -0
- package/src/codex/catalog-write-serialization.ts +71 -7
- package/src/codex/codex-home-owner.ts +124 -0
- package/src/codex/convergence.ts +55 -15
- package/src/codex/features.ts +8 -1
- package/src/codex/inject/restore.ts +46 -4
- package/src/codex/inject.ts +11 -1
- package/src/codex/internal/catalog-writer.ts +120 -13
- package/src/codex/journal.ts +49 -13
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/quota-types.ts +3 -0
- package/src/codex/quota.ts +13 -3
- package/src/codex/refresh.ts +20 -9
- package/src/codex/runtime.ts +7 -1
- package/src/codex/shim-diagnostics.ts +80 -0
- package/src/codex/shim-fingerprint.ts +7 -2
- package/src/codex/shim-migration.ts +153 -0
- package/src/codex/shim-overlay.ts +509 -0
- package/src/codex/shim-path-resolution.ts +14 -2
- package/src/codex/shim-probe.ts +4 -3
- package/src/codex/shim-state-file.ts +93 -7
- package/src/codex/shim.ts +15 -68
- package/src/codex/sync.ts +24 -4
- package/src/combos/failover.ts +19 -0
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +12 -0
- package/src/config/atomic-write.ts +13 -8
- package/src/config/diagnostics.ts +19 -1
- package/src/config/load-degrade.ts +3 -0
- package/src/config/schema/chatgpt-desktop.ts +22 -0
- package/src/config/schema/config-schema.ts +32 -14
- package/src/config/schema/leaf-validators.ts +3 -2
- package/src/generated/compatibility-version.json +445 -129
- package/src/integrations/merge.ts +13 -5
- package/src/integrations/mutation-plan.ts +46 -3
- package/src/integrations/omp-yaml-source.ts +181 -1
- package/src/integrations/ownership-policy.ts +66 -0
- package/src/integrations/registry.ts +50 -1
- package/src/integrations/state.ts +46 -21
- package/src/integrations/target.ts +25 -2
- package/src/integrations/writer.ts +89 -31
- package/src/lib/package-tree-integrity.ts +42 -1
- package/src/lib/proxy-env.ts +3 -0
- package/src/lib/test-home-guard.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +19 -8
- package/src/oauth/anthropic-routing.ts +17 -12
- package/src/oauth/anthropic.ts +27 -2
- package/src/oauth/index.ts +6 -8
- package/src/oauth/pool-settings-capability.ts +9 -2
- package/src/protocols/plan-snapshot.ts +1 -1
- package/src/protocols/settings.ts +28 -4
- package/src/server/caller-session-identity.ts +32 -0
- package/src/server/claude-messages.ts +11 -4
- package/src/server/index/serve-options.ts +12 -7
- package/src/server/index.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +6 -5
- package/src/server/management/context.ts +8 -1
- package/src/server/management/cursor-integration-routes.ts +3 -3
- package/src/server/management/integration-routes.ts +5 -5
- package/src/server/management/model-routes.ts +3 -3
- package/src/server/management/native-integration-routes.ts +2 -2
- package/src/server/management/oauth-account-routes.ts +96 -30
- package/src/server/management/route-registry.ts +25 -24
- package/src/server/messages-native-eligibility.ts +3 -6
- package/src/server/messages-native-oauth.ts +82 -35
- package/src/server/messages-native.ts +78 -16
- package/src/server/responses/adapter-dispatch.ts +53 -28
- package/src/server/responses/core-auth.ts +10 -4
- package/src/server/responses/core-combo.ts +46 -6
- package/src/service/cli.ts +6 -1
- package/src/service/ownership-mutation-lease.d.mts +26 -0
- package/src/service/ownership-mutation-lease.mjs +76 -1
- package/src/types/config.ts +2 -0
- package/src/types/provider.ts +10 -10
- package/src/update/index.ts +2 -1
- package/src/update/update-failure-guidance.d.mts +6 -0
- package/src/update/update-failure-guidance.mjs +30 -5
- package/gui/dist/assets/App-COdEeqSj.css +0 -1
- package/gui/dist/assets/App-DEpCJEP_.js +0 -54
- package/gui/dist/assets/index-Cd7ittMv.css +0 -1
- package/gui/dist/assets/index-DwJKFJm5.js +0 -94
- package/gui/dist/assets/quota-summary-DGcEJU3h.js +0 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readConfigFileSnapshot, validateConfigCandidate } from "../../config/diagnostics";
|
|
2
|
+
import { ConfigWritePublishedError } from "../../config/persist-unlocked";
|
|
1
3
|
import { parseAnthropicModelRoutes, readAnthropicModelRoutes } from "../../oauth/anthropic-model-routes";
|
|
2
4
|
import { effectiveAnthropicAccountThreshold } from "../../oauth/anthropic-account-threshold";
|
|
3
5
|
import { handleAnthropicAccountThreshold } from "./anthropic-account-threshold";
|
|
@@ -16,6 +18,7 @@ import {
|
|
|
16
18
|
readConfigDiagnostics,
|
|
17
19
|
reconcileLiveConfigFromDisk,
|
|
18
20
|
saveConfigPreservingClaudeCode,
|
|
21
|
+
mutatePersistedConfig,
|
|
19
22
|
} from "../../config";
|
|
20
23
|
import {
|
|
21
24
|
clearLoginState,
|
|
@@ -210,6 +213,44 @@ function genericOAuthProviderConfig(provider: string, config: ManagementContext[
|
|
|
210
213
|
return definition?.resolveProviderConfig?.(config) ?? definition?.providerConfig;
|
|
211
214
|
}
|
|
212
215
|
|
|
216
|
+
/** Both Anthropic writers share publication-aware recovery from the atomic mutation owner. */
|
|
217
|
+
function persistAnthropicPoolPatch(
|
|
218
|
+
config: OcxConfig,
|
|
219
|
+
patch: (target: OcxConfig) => { changed: boolean; value: NonNullable<OcxConfig["anthropicAccountPool"]> },
|
|
220
|
+
): { status: "saved"; warning?: "config_bookkeeping_failed" } | { status: "failed"; response: Response } {
|
|
221
|
+
const before = readConfigFileSnapshot();
|
|
222
|
+
const unknown = () => ({ status: "failed" as const, response: jsonResponse({
|
|
223
|
+
error: "Pool settings save state is unknown; reload settings before editing again",
|
|
224
|
+
code: "config_save_state_unknown",
|
|
225
|
+
}, 409) });
|
|
226
|
+
try {
|
|
227
|
+
const saved = mutatePersistedConfig(patch);
|
|
228
|
+
if (saved.status === "unavailable") return unknown();
|
|
229
|
+
config.anthropicAccountPool = saved.value;
|
|
230
|
+
reconcileLiveStateStores();
|
|
231
|
+
return { status: "saved" };
|
|
232
|
+
} catch (error) {
|
|
233
|
+
const persisted = readConfigFileSnapshot();
|
|
234
|
+
if (persisted.diagnostics.source !== "file" || persisted.raw === undefined) return unknown();
|
|
235
|
+
// File reads may salvage hand edits; recovery success requires a strict, authoritative document.
|
|
236
|
+
let validated: ReturnType<typeof validateConfigCandidate>;
|
|
237
|
+
try { validated = validateConfigCandidate(JSON.parse(persisted.raw.replace(/^\uFEFF/, ""))); }
|
|
238
|
+
catch { return unknown(); }
|
|
239
|
+
if (!validated.ok) return unknown();
|
|
240
|
+
const current = validated.config;
|
|
241
|
+
const expected = patch(structuredClone(current)).value;
|
|
242
|
+
const matches = JSON.stringify(current.anthropicAccountPool) === JSON.stringify(expected);
|
|
243
|
+
if (matches) {
|
|
244
|
+
config.anthropicAccountPool = current.anthropicAccountPool;
|
|
245
|
+
// A failed reconcile must not turn confirmed publication back into a rollback.
|
|
246
|
+
try { reconcileLiveStateStores(); } catch { /* Fixed warning covers bookkeeping failure. */ }
|
|
247
|
+
return { status: "saved", warning: "config_bookkeeping_failed" };
|
|
248
|
+
}
|
|
249
|
+
if (error instanceof ConfigWritePublishedError || persisted.raw !== before.raw) return unknown();
|
|
250
|
+
return { status: "failed", response: jsonResponse({ error: "Pool settings could not be saved" }, 500) };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
213
254
|
export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
214
255
|
const { req, url, config, deps, principal, syncClaudeAgentDefsBestEffort } = ctx;
|
|
215
256
|
|
|
@@ -565,7 +606,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
565
606
|
if (req.method !== "GET" && !isPlainRecord(rawBody)) {
|
|
566
607
|
return jsonResponse({ error: "body must be an object" }, 400);
|
|
567
608
|
}
|
|
568
|
-
const fields = rawBody as { provider?: unknown; enabled?: unknown; strategy?: unknown; stickyLimit?: unknown; autoSwitchThreshold?: unknown; quotaWindow?: unknown; maxConcurrentPerAccount?: unknown; routes?: unknown };
|
|
609
|
+
const fields = rawBody as { provider?: unknown; enabled?: unknown; strategy?: unknown; stickyLimit?: unknown; autoSwitchThreshold?: unknown; quotaWindow?: unknown; maxConcurrentPerAccount?: unknown; routes?: unknown; nativeMessages?: unknown };
|
|
569
610
|
const provider = req.method === "GET"
|
|
570
611
|
? (url.searchParams.get("provider") ?? "").trim().toLowerCase()
|
|
571
612
|
: (typeof fields.provider === "string" ? fields.provider.trim().toLowerCase() : "");
|
|
@@ -573,6 +614,10 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
573
614
|
if (!provider || !kind) {
|
|
574
615
|
return jsonResponse({ error: "pool settings are only available for the codex, anthropic and generic OAuth pools" }, 400);
|
|
575
616
|
}
|
|
617
|
+
if (Object.hasOwn(fields, "nativeMessages")) {
|
|
618
|
+
if (kind !== "anthropic") return jsonResponse({ error: "nativeMessages is only part of the anthropic pool contract" }, 400);
|
|
619
|
+
if (typeof fields.nativeMessages !== "boolean") return jsonResponse({ error: "nativeMessages must be a boolean" }, 400);
|
|
620
|
+
}
|
|
576
621
|
// Validated by the SHARED parsers before any kind-specific write, so a bad strategy or
|
|
577
622
|
// sticky limit is refused identically whichever pool is addressed.
|
|
578
623
|
let strategy: string | undefined;
|
|
@@ -625,17 +670,24 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
625
670
|
if (stickyLimit !== undefined) config.accountPoolStickyLimit = stickyLimit;
|
|
626
671
|
if (autoSwitchThreshold !== undefined) config.autoSwitchThreshold = autoSwitchThreshold;
|
|
627
672
|
} else if (kind === "anthropic") {
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
if (
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
673
|
+
const patchPool = (target: OcxConfig) => {
|
|
674
|
+
const pool = { ...(target.anthropicAccountPool ?? {}) };
|
|
675
|
+
if (fields.nativeMessages !== undefined) pool.nativeMessages = fields.nativeMessages as boolean;
|
|
676
|
+
if (fields.enabled !== undefined) pool.enabled = fields.enabled as boolean;
|
|
677
|
+
if (strategy !== undefined) pool.strategy = strategy as never;
|
|
678
|
+
if (stickyLimit !== undefined) pool.stickyLimit = stickyLimit;
|
|
679
|
+
if (autoSwitchThreshold !== undefined) pool.autoSwitchThreshold = autoSwitchThreshold;
|
|
680
|
+
if (quotaWindow !== undefined) pool.quotaWindow = quotaWindow as never;
|
|
681
|
+
if (Object.hasOwn(fields, "routes")) {
|
|
682
|
+
if (fields.routes === null) delete pool.routes;
|
|
683
|
+
else if (parsedRoutes?.ok) pool.routes = parsedRoutes.routes;
|
|
684
|
+
}
|
|
685
|
+
target.anthropicAccountPool = pool;
|
|
686
|
+
return { changed: true, value: pool };
|
|
687
|
+
};
|
|
688
|
+
const saved = persistAnthropicPoolPatch(config, patchPool);
|
|
689
|
+
if (saved.status === "failed") return saved.response;
|
|
690
|
+
return jsonResponse({ ...unifiedPoolSettingsDto(config, provider, kind), ...(saved.warning ? { warning: saved.warning } : {}) });
|
|
639
691
|
} else {
|
|
640
692
|
const prov = config.providers[provider]!;
|
|
641
693
|
const next = { ...(prov.oauthAccountFailover ?? {}) };
|
|
@@ -679,6 +731,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
679
731
|
stickyLimit: normalizeAccountPoolStickyLimit(pool.stickyLimit),
|
|
680
732
|
quotaWindow: normalizeAccountPoolQuotaWindow(pool.quotaWindow),
|
|
681
733
|
...readAnthropicModelRoutes(pool.routes),
|
|
734
|
+
nativeMessages: !Object.hasOwn(pool, "nativeMessages") || pool.nativeMessages === true,
|
|
682
735
|
experimental: true,
|
|
683
736
|
});
|
|
684
737
|
}
|
|
@@ -696,8 +749,13 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
696
749
|
quotaWindow?: unknown;
|
|
697
750
|
maxConcurrentPerAccount?: unknown;
|
|
698
751
|
routes?: unknown;
|
|
752
|
+
nativeMessages?: unknown;
|
|
699
753
|
};
|
|
700
754
|
const provider = typeof body.provider === "string" ? body.provider.trim().toLowerCase() : "";
|
|
755
|
+
if (Object.hasOwn(body, "nativeMessages")) {
|
|
756
|
+
if (provider !== "anthropic") return jsonResponse({ error: "nativeMessages is only part of the anthropic pool contract" }, 400);
|
|
757
|
+
if (typeof body.nativeMessages !== "boolean") return jsonResponse({ error: "nativeMessages must be a boolean" }, 400);
|
|
758
|
+
}
|
|
701
759
|
if (provider !== "anthropic") {
|
|
702
760
|
const {
|
|
703
761
|
poolSettingsCapability, genericPoolSettingsDto, parseGenericPoolStrategy, parseGenericAutoSwitchThreshold,
|
|
@@ -796,28 +854,36 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
796
854
|
const parsedLegacyRoutes = Object.hasOwn(body, "routes") && body.routes !== null
|
|
797
855
|
? parseAnthropicModelRoutes(body.routes) : null;
|
|
798
856
|
if (parsedLegacyRoutes && !parsedLegacyRoutes.ok) return jsonResponse({ error: parsedLegacyRoutes.error }, 400);
|
|
799
|
-
const
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
autoSwitchThreshold
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
857
|
+
const legacyPatch = (target: OcxConfig) => {
|
|
858
|
+
const pool = { ...(target.anthropicAccountPool ?? {}) };
|
|
859
|
+
if (body.enabled !== undefined) pool.enabled = enabled;
|
|
860
|
+
else if (pool.enabled === undefined) pool.enabled = false;
|
|
861
|
+
if (body.nativeMessages !== undefined) pool.nativeMessages = body.nativeMessages as boolean;
|
|
862
|
+
if (body.autoSwitchThreshold !== undefined) pool.autoSwitchThreshold = threshold;
|
|
863
|
+
else if (pool.autoSwitchThreshold === undefined) pool.autoSwitchThreshold = 80;
|
|
864
|
+
if (body.strategy !== undefined) pool.strategy = strategy;
|
|
865
|
+
if (body.stickyLimit !== undefined) pool.stickyLimit = stickyLimit;
|
|
866
|
+
if (body.quotaWindow !== undefined) pool.quotaWindow = quotaWindow;
|
|
867
|
+
if (Object.hasOwn(body, "routes")) {
|
|
868
|
+
if (body.routes === null) delete pool.routes;
|
|
869
|
+
else if (parsedLegacyRoutes?.ok) pool.routes = parsedLegacyRoutes.routes;
|
|
870
|
+
}
|
|
871
|
+
target.anthropicAccountPool = pool;
|
|
872
|
+
return { changed: true, value: pool };
|
|
809
873
|
};
|
|
810
|
-
|
|
811
|
-
|
|
874
|
+
const saved = persistAnthropicPoolPatch(config, legacyPatch);
|
|
875
|
+
if (saved.status === "failed") return saved.response;
|
|
812
876
|
return jsonResponse({
|
|
813
877
|
ok: true,
|
|
814
878
|
provider,
|
|
815
|
-
enabled,
|
|
816
|
-
autoSwitchThreshold:
|
|
817
|
-
strategy: normalizeAccountPoolStrategy(strategy),
|
|
818
|
-
stickyLimit: normalizeAccountPoolStickyLimit(stickyLimit),
|
|
819
|
-
quotaWindow: normalizeAccountPoolQuotaWindow(quotaWindow),
|
|
820
|
-
routes: routes ?? null,
|
|
879
|
+
enabled: config.anthropicAccountPool?.enabled === true,
|
|
880
|
+
autoSwitchThreshold: config.anthropicAccountPool?.autoSwitchThreshold ?? 80,
|
|
881
|
+
strategy: normalizeAccountPoolStrategy(config.anthropicAccountPool?.strategy),
|
|
882
|
+
stickyLimit: normalizeAccountPoolStickyLimit(config.anthropicAccountPool?.stickyLimit),
|
|
883
|
+
quotaWindow: normalizeAccountPoolQuotaWindow(config.anthropicAccountPool?.quotaWindow),
|
|
884
|
+
routes: config.anthropicAccountPool?.routes ?? null,
|
|
885
|
+
nativeMessages: !Object.hasOwn(config.anthropicAccountPool ?? {}, "nativeMessages") || config.anthropicAccountPool?.nativeMessages === true,
|
|
886
|
+
...(saved.warning ? { warning: saved.warning } : {}),
|
|
821
887
|
experimental: true,
|
|
822
888
|
});
|
|
823
889
|
}
|
|
@@ -30,6 +30,8 @@ export type ExemptionReason =
|
|
|
30
30
|
| "gui-invalidation"
|
|
31
31
|
/** Requires a dashboard browser session. Includes the user-consent star boundary. */
|
|
32
32
|
| "session-only"
|
|
33
|
+
/** Opens a browser view; data operations remain independently CLI-addressable. */
|
|
34
|
+
| "browser-navigation"
|
|
33
35
|
/** Deliberately returns 405; there is nothing to drive. */
|
|
34
36
|
| "disabled"
|
|
35
37
|
/** Gated on a process-scoped capability principal, not an operator action. */
|
|
@@ -43,10 +45,8 @@ export type ExemptionReason =
|
|
|
43
45
|
/** Older clients use this alias; the current CLI drives its declared replacement. */
|
|
44
46
|
| "compatibility-alias"
|
|
45
47
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* only meaningful to the caller that is about to commit it, and a scripted caller drives the
|
|
49
|
-
* mutation directly.
|
|
48
|
+
* An interactive-only preview that lacks a supported terminal confirmation workflow.
|
|
49
|
+
* Named CLI preview/commit workflows are operational capabilities, not this exemption.
|
|
50
50
|
*/
|
|
51
51
|
| "interactive-preview"
|
|
52
52
|
/** Unreachable in the live dispatch order; delete rather than expose. */
|
|
@@ -121,7 +121,7 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
121
121
|
{ method: "PUT", path: "/api/codex-auth/accounts/alias", module: "codex/auth-api/routes", mutates: true },
|
|
122
122
|
{ method: "PUT", path: "/api/codex-auth/accounts/pause", module: "codex/auth-api/routes", mutates: true },
|
|
123
123
|
{ method: "PUT", path: "/api/codex-auth/accounts/pause-exhausted", module: "codex/auth-api/routes", mutates: true },
|
|
124
|
-
{ method: "PUT", path: "/api/codex-auth/accounts/credits", module: "codex/auth-api/routes", mutates: true
|
|
124
|
+
{ method: "PUT", path: "/api/codex-auth/accounts/credits", module: "codex/auth-api/routes", mutates: true },
|
|
125
125
|
{ method: "PUT", path: "/api/codex-auth/accounts/priority", module: "codex/auth-api/routes", mutates: true },
|
|
126
126
|
{ method: "PUT", path: "/api/codex-auth/active", module: "codex/auth-api/routes", mutates: true },
|
|
127
127
|
{ method: "PUT", path: "/api/codex-auth/auto-switch", module: "codex/auth-api/routes", mutates: true },
|
|
@@ -152,7 +152,7 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
152
152
|
{ method: "POST", path: "/api/grok/apply", module: "server/management/agent-settings-routes", mutates: true },
|
|
153
153
|
{ method: "GET", path: "/api/grok/reset-coupons", module: "server/management/grok-coupon-routes", mutates: false },
|
|
154
154
|
{ method: "POST", path: "/api/grok/reset-coupons/consume", module: "server/management/grok-coupon-routes", mutates: true },
|
|
155
|
-
{ method: "GET", path: "/api/anthropic/reset-grants", module: "server/management/anthropic-reset-grant-routes", mutates: false
|
|
155
|
+
{ method: "GET", path: "/api/anthropic/reset-grants", module: "server/management/anthropic-reset-grant-routes", mutates: false },
|
|
156
156
|
{ 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." } },
|
|
157
157
|
{ method: "PUT", path: "/api/claude-code", module: "server/management/agent-settings-routes", mutates: true },
|
|
158
158
|
{ method: "PUT", path: "/api/claude-desktop", module: "server/management/agent-settings-routes", mutates: true },
|
|
@@ -185,10 +185,10 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
185
185
|
{ method: "GET", path: "/api/client-integrations/aside/profiles/{profileId}", module: "server/management/aside-profile-routes", mutates: false, mechanism: "prefix-decode" },
|
|
186
186
|
{ method: "PUT", path: "/api/client-integrations/aside/profiles/{profileId}", module: "server/management/aside-profile-routes", mutates: true, mechanism: "prefix-decode" },
|
|
187
187
|
{ method: "GET", path: "/api/client-integrations/aside/profiles/journal", module: "server/management/aside-profile-routes", mutates: false, mechanism: "prefix-decode" },
|
|
188
|
-
{ method: "DELETE", path: "/api/client-integrations/aside/profiles/journal", module: "server/management/aside-profile-routes", mutates: true, mechanism: "prefix-decode"
|
|
188
|
+
{ method: "DELETE", path: "/api/client-integrations/aside/profiles/journal", module: "server/management/aside-profile-routes", mutates: true, mechanism: "prefix-decode" },
|
|
189
189
|
{ method: "GET", path: "/api/client-integrations/aside/profiles/{profileId}/journal", module: "server/management/aside-profile-routes", mutates: false, mechanism: "prefix-decode" },
|
|
190
|
-
{ method: "POST", path: "/api/client-integrations/aside/profiles/{profileId}/preview", module: "server/management/aside-profile-routes", mutates: false, mechanism: "prefix-decode"
|
|
191
|
-
{ method: "DELETE", path: "/api/client-integrations/aside/profiles/{profileId}/journal", module: "server/management/aside-profile-routes", mutates: true, mechanism: "prefix-decode"
|
|
190
|
+
{ method: "POST", path: "/api/client-integrations/aside/profiles/{profileId}/preview", module: "server/management/aside-profile-routes", mutates: false, mechanism: "prefix-decode" },
|
|
191
|
+
{ method: "DELETE", path: "/api/client-integrations/aside/profiles/{profileId}/journal", module: "server/management/aside-profile-routes", mutates: true, mechanism: "prefix-decode" },
|
|
192
192
|
{ method: "POST", path: "/api/client-integrations/aside/profiles/{profileId}/restore", module: "server/management/aside-profile-routes", mutates: true, mechanism: "prefix-decode" },
|
|
193
193
|
// server/management/codex-prompt-routes
|
|
194
194
|
{ method: "GET", path: "/api/codex-prompt", module: "server/management/codex-prompt-routes", mutates: false },
|
|
@@ -227,15 +227,15 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
227
227
|
// server/management/integration-routes
|
|
228
228
|
{ method: "GET", path: "/api/client-integrations", module: "server/management/integration-routes", mutates: false },
|
|
229
229
|
{ method: "GET", path: "/api/client-integrations/journal", module: "server/management/integration-routes", mutates: false },
|
|
230
|
-
{ method: "DELETE", path: "/api/client-integrations/journal", module: "server/management/integration-routes", mutates: true
|
|
230
|
+
{ method: "DELETE", path: "/api/client-integrations/journal", module: "server/management/integration-routes", mutates: true },
|
|
231
231
|
{ method: "POST", path: "/api/client-integrations/restore", module: "server/management/integration-routes", mutates: true },
|
|
232
|
-
{ method: "POST", path: "/api/client-integrations/preview", module: "server/management/integration-routes", mutates: false
|
|
233
|
-
{ method: "POST", path: "/api/client-integrations/restore/preview", module: "server/management/integration-routes", mutates: false
|
|
232
|
+
{ method: "POST", path: "/api/client-integrations/preview", module: "server/management/integration-routes", mutates: false },
|
|
233
|
+
{ method: "POST", path: "/api/client-integrations/restore/preview", module: "server/management/integration-routes", mutates: false },
|
|
234
234
|
// server/management/lab-automation-routes
|
|
235
235
|
{ method: "GET", path: "/api/lab/automation", module: "server/management/lab-automation-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
|
|
236
236
|
{ method: "GET", path: "/api/lab/automation/runs", module: "server/management/lab-automation-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
|
|
237
|
-
{ method: "POST", path: "/api/lab/automation/run", module: "server/management/lab-automation-routes", mutates: true, exempt: { reason: "deferred-verb", why: "
|
|
238
|
-
{ method: "PUT", path: "/api/lab/automation", module: "server/management/lab-automation-routes", mutates: true, exempt: { reason: "deferred-verb", why: "
|
|
237
|
+
{ method: "POST", path: "/api/lab/automation/run", module: "server/management/lab-automation-routes", mutates: true, exempt: { reason: "deferred-verb", why: "ocx lab run explicitly executes a local scenario. The local invocation does not control the selected running proxy; this HTTP operation remains deferred.", owner: "wp7", ownerDoc: "devlog/_plan/260828_ocx_agentic_control/060_phase_gui_parity.md" } },
|
|
238
|
+
{ method: "PUT", path: "/api/lab/automation", module: "server/management/lab-automation-routes", mutates: true, exempt: { reason: "deferred-verb", why: "ocx lab automation enable/disable updates local policy. The local invocation does not control the selected running proxy; this HTTP operation remains deferred.", owner: "wp7", ownerDoc: "devlog/_plan/260828_ocx_agentic_control/060_phase_gui_parity.md" } },
|
|
239
239
|
// server/management/lab-routes
|
|
240
240
|
{ method: "GET", path: "/api/lab/artifacts", module: "server/management/lab-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
|
|
241
241
|
{ method: "GET", path: "/api/lab/catalog", module: "server/management/lab-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
|
|
@@ -246,10 +246,10 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
246
246
|
{ method: "GET", path: "/api/lab/status", module: "server/management/lab-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
|
|
247
247
|
{ method: "GET", path: "/api/lab/subjects", module: "server/management/lab-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
|
|
248
248
|
{ method: "GET", path: "/api/lab/verdicts", module: "server/management/lab-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
|
|
249
|
-
{ method: "POST", path: "/api/lab/public/community/import", module: "server/management/lab-routes", mutates: true, exempt: { reason: "deferred-verb", why: "
|
|
250
|
-
{ method: "POST", path: "/api/lab/public/export", module: "server/management/lab-routes", mutates: true, exempt: { reason: "deferred-verb", why: "
|
|
251
|
-
{ method: "POST", path: "/api/lab/public/preview", module: "server/management/lab-routes", mutates: true, exempt: { reason: "deferred-verb", why: "
|
|
252
|
-
{ method: "POST", path: "/api/lab/public/verify", module: "server/management/lab-routes", mutates: true, exempt: { reason: "deferred-verb", why: "
|
|
249
|
+
{ method: "POST", path: "/api/lab/public/community/import", module: "server/management/lab-routes", mutates: true, exempt: { reason: "deferred-verb", why: "ocx lab public import reads a local bundle file. The local invocation does not control the selected running proxy; this HTTP operation remains deferred.", owner: "wp7", ownerDoc: "devlog/_plan/260828_ocx_agentic_control/060_phase_gui_parity.md" } },
|
|
250
|
+
{ method: "POST", path: "/api/lab/public/export", module: "server/management/lab-routes", mutates: true, exempt: { reason: "deferred-verb", why: "ocx lab public export uses local evidence. The local invocation does not control the selected running proxy; this HTTP operation remains deferred.", owner: "wp7", ownerDoc: "devlog/_plan/260828_ocx_agentic_control/060_phase_gui_parity.md" } },
|
|
251
|
+
{ method: "POST", path: "/api/lab/public/preview", module: "server/management/lab-routes", mutates: true, exempt: { reason: "deferred-verb", why: "ocx lab public preview uses local evidence. The local invocation does not control the selected running proxy; this HTTP operation remains deferred.", owner: "wp7", ownerDoc: "devlog/_plan/260828_ocx_agentic_control/060_phase_gui_parity.md" } },
|
|
252
|
+
{ method: "POST", path: "/api/lab/public/verify", module: "server/management/lab-routes", mutates: true, exempt: { reason: "deferred-verb", why: "ocx lab public verify reads a local bundle file. The local invocation does not control the selected running proxy; this HTTP operation remains deferred.", owner: "wp7", ownerDoc: "devlog/_plan/260828_ocx_agentic_control/060_phase_gui_parity.md" } },
|
|
253
253
|
// server/management/logs-usage-routes
|
|
254
254
|
{ method: "GET", path: "/api/claude/inbound-debug", module: "server/management/logs-usage-routes", mutates: false },
|
|
255
255
|
{ method: "GET", path: "/api/debug", module: "server/management/logs-usage-routes", mutates: false },
|
|
@@ -265,7 +265,7 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
265
265
|
// server/management/usage-timeline-routes
|
|
266
266
|
{ method: "GET", path: "/api/usage/timeline", module: "server/management/usage-timeline-routes", mutates: false },
|
|
267
267
|
// server/management/companion-routes
|
|
268
|
-
{ method: "POST", path: "/api/companion/open-in-browser", module: "server/management/companion-routes", mutates: true, exempt: { reason: "
|
|
268
|
+
{ method: "POST", path: "/api/companion/open-in-browser", module: "server/management/companion-routes", mutates: true, exempt: { reason: "browser-navigation", why: "Opens the companion's current view in the system browser under normal management admission; the CLI reads the underlying settings and usage directly." } },
|
|
269
269
|
{ method: "GET", path: "/api/companion/settings", module: "server/management/companion-routes", mutates: false },
|
|
270
270
|
{ method: "PUT", path: "/api/companion/settings", module: "server/management/companion-routes", mutates: true },
|
|
271
271
|
{ method: "POST", path: "/api/storage/cleanup", module: "server/management/logs-usage-routes", mutates: true },
|
|
@@ -315,7 +315,7 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
315
315
|
{ method: "GET", path: "/api/oauth/accounts/pool", module: "server/management/oauth-account-routes", mutates: false },
|
|
316
316
|
{ method: "GET", path: "/api/pool/settings", module: "server/management/oauth-account-routes", mutates: false },
|
|
317
317
|
{ method: "PUT", path: "/api/pool/settings", module: "server/management/oauth-account-routes", mutates: true },
|
|
318
|
-
{ method: "PATCH", path: "/api/pool/settings", module: "server/management/oauth-account-routes", mutates: true },
|
|
318
|
+
{ method: "PATCH", path: "/api/pool/settings", module: "server/management/oauth-account-routes", mutates: true, exempt: { reason: "compatibility-alias", why: "Handled by the same partial settings branch as PUT /api/pool/settings; current CLI strategy, sticky and routes commands use PUT, not PATCH." } },
|
|
319
319
|
{ method: "GET", path: "/api/oauth/providers", module: "server/management/oauth-account-routes", mutates: false },
|
|
320
320
|
{ method: "GET", path: "/api/oauth/status", module: "server/management/oauth-account-routes", mutates: false },
|
|
321
321
|
{ method: "GET", path: "/api/providers/keys", module: "server/management/oauth-account-routes", mutates: false },
|
|
@@ -350,7 +350,7 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
350
350
|
{ method: "PATCH", path: "/api/providers", module: "server/management/provider-routes", mutates: true },
|
|
351
351
|
{ method: "POST", path: "/api/providers", module: "server/management/provider-routes", mutates: true },
|
|
352
352
|
{ method: "POST", path: "/api/providers/test", module: "server/management/provider-routes", mutates: true },
|
|
353
|
-
{ method: "PUT", path: "/api/providers", module: "server/management/provider-routes", mutates: true
|
|
353
|
+
{ method: "PUT", path: "/api/providers", module: "server/management/provider-routes", mutates: true },
|
|
354
354
|
{ method: "PUT", path: "/api/provider-context-caps", module: "server/management/provider-routes", mutates: true },
|
|
355
355
|
// server/management/quota-reset-routes
|
|
356
356
|
{ method: "GET", path: "/api/quota-resets", module: "server/management/quota-reset-routes", mutates: false, mechanism: "negated-guard" },
|
|
@@ -394,9 +394,9 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
394
394
|
{ method: "DELETE", path: "/api/link/{id}", module: "server/management/link-routes", mutates: true, mechanism: "regex" },
|
|
395
395
|
{ method: "POST", path: "/api/link/issue", module: "server/management/link-routes", mutates: true },
|
|
396
396
|
// server/management/remote-workspace-routes
|
|
397
|
-
{ method: "GET", path: "/api/remote-workspace", module: "server/management/remote-workspace-routes", mutates: false
|
|
398
|
-
{ method: "GET", path: "/api/remote-workspace/runtimes", module: "server/management/remote-workspace-routes", mutates: false
|
|
399
|
-
{ method: "GET", path: "/api/remote-workspace/sessions", module: "server/management/remote-workspace-routes", mutates: false
|
|
397
|
+
{ method: "GET", path: "/api/remote-workspace", module: "server/management/remote-workspace-routes", mutates: false },
|
|
398
|
+
{ method: "GET", path: "/api/remote-workspace/runtimes", module: "server/management/remote-workspace-routes", mutates: false },
|
|
399
|
+
{ method: "GET", path: "/api/remote-workspace/sessions", module: "server/management/remote-workspace-routes", mutates: false },
|
|
400
400
|
{ method: "POST", path: "/api/remote-workspace/pairing", module: "server/management/remote-workspace-routes", mutates: true, exempt: { reason: "session-only", why: "Creating a device enrollment grant authorizes another computer, so only a dashboard consent session may request one." } },
|
|
401
401
|
{ method: "POST", path: "/api/remote-workspace/sessions", module: "server/management/remote-workspace-routes", mutates: true, exempt: { reason: "session-only", why: "Starting a Hub-authenticated model session against a remote computer requires an interactive dashboard consent session." } },
|
|
402
402
|
{ method: "POST", path: "/api/remote-workspace/sessions/{id}/prompt", module: "server/management/remote-workspace-routes", mutates: true, mechanism: "regex", exempt: { reason: "session-only", why: "A prompt can execute tools on the selected remote computer and therefore requires an interactive dashboard consent session." } },
|
|
@@ -422,6 +422,7 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
422
422
|
{ method: "PUT", path: "/api/providers/{provider}/model-aliases", module: "server/management/model-routes", mutates: true, mechanism: "regex" },
|
|
423
423
|
{ method: "GET", path: "/api/providers/{provider}/model-costs", module: "server/management/model-routes", mutates: false, mechanism: "regex" },
|
|
424
424
|
{ method: "PUT", path: "/api/providers/{provider}/model-costs", module: "server/management/model-routes", mutates: true, mechanism: "regex" },
|
|
425
|
+
{ method: "PUT", path: "/api/providers/{provider}/model-display-names", module: "server/management/model-routes", mutates: true, mechanism: "regex" },
|
|
425
426
|
{ method: "PUT", path: "/api/custom-models/{id}", module: "server/management/model-routes", mutates: true, mechanism: "regex" },
|
|
426
427
|
{ method: "DELETE", path: "/api/custom-models/{id}", module: "server/management/model-routes", mutates: true, mechanism: "regex" },
|
|
427
428
|
{ method: "GET", path: "/api/lab/subjects/{id}", module: "server/management/lab-routes", mutates: false, mechanism: "regex", exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { anthropicBodyElidesBlockedSkill } from "../claude/inbound";
|
|
9
9
|
import { isClaudeWebSearchToolName } from "../claude/outbound";
|
|
10
|
-
import { isAnthropicAccountPoolEnabled } from "../oauth/anthropic-routing";
|
|
11
10
|
import type { ProtocolReasonCode } from "../protocols/contract";
|
|
12
11
|
import { featuresFromMessagesBody } from "../protocols/features";
|
|
13
12
|
import { credentialDomainFor } from "../protocols/opaque-state";
|
|
@@ -96,8 +95,7 @@ function bridgeOnlyPolicyApplies(
|
|
|
96
95
|
* The credential rule. A proxy-managed key is native since PF-08. An Anthropic OAuth account
|
|
97
96
|
* is native only with `managedMessagesNativeOAuth` on (itself effective only with
|
|
98
97
|
* `managedMessagesNative`), only for the `anthropic` provider the OAuth store serves, and only
|
|
99
|
-
* to `api.anthropic.com`.
|
|
100
|
-
* ranking live in the Responses pipeline's transport and are not replicated here. `forward`
|
|
98
|
+
* to `api.anthropic.com`. Pool selection and recovery are resolved at native dispatch. `forward`
|
|
101
99
|
* belongs to the caller.
|
|
102
100
|
*/
|
|
103
101
|
function credentialDeclineReason(
|
|
@@ -108,10 +106,9 @@ function credentialDeclineReason(
|
|
|
108
106
|
const provider = route.provider;
|
|
109
107
|
if (provider.authMode === undefined || provider.authMode === "key") return undefined;
|
|
110
108
|
if (provider.authMode !== "oauth") return "auth-mode-not-native";
|
|
111
|
-
if (!resolveProtocolSettings(config).rollout.managedMessagesNativeOAuth) return "auth-mode-not-native";
|
|
109
|
+
if (!resolveProtocolSettings(config, route.providerName).rollout.managedMessagesNativeOAuth) return "auth-mode-not-native";
|
|
112
110
|
if (route.providerName !== "anthropic") return "auth-mode-not-native";
|
|
113
111
|
if (!credentialDomainFor(provider)?.firstPartyAnthropic) return "auth-mode-not-native";
|
|
114
|
-
if (isAnthropicAccountPoolEnabled(config) || selector.oauthFailoverQuorum === true) return "oauth-account-pool";
|
|
115
112
|
return undefined;
|
|
116
113
|
}
|
|
117
114
|
|
|
@@ -135,7 +132,7 @@ export function nativeMessagesDeclineReason(
|
|
|
135
132
|
config: OcxConfig,
|
|
136
133
|
selector: NativeMessagesSelector = {},
|
|
137
134
|
): NativeMessagesDeclineReason | undefined {
|
|
138
|
-
if (!resolveProtocolSettings(config).rollout.managedMessagesNative) return "rollout-disabled";
|
|
135
|
+
if (!resolveProtocolSettings(config, route.providerName).rollout.managedMessagesNative) return "rollout-disabled";
|
|
139
136
|
const provider = route.provider;
|
|
140
137
|
if (provider.adapter !== "anthropic") return "cross-wire-ir";
|
|
141
138
|
const credentialDecline = credentialDeclineReason(route, config, selector);
|
|
@@ -2,19 +2,15 @@
|
|
|
2
2
|
* Anthropic OAuth on the managed native Messages lane (PF-10, behind
|
|
3
3
|
* `protocols.rollout.managedMessagesNativeOAuth`).
|
|
4
4
|
*
|
|
5
|
-
* Credential.
|
|
6
|
-
* the same
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* proposal against the captured selection so a concurrent manual switch wins. Nothing here picks
|
|
10
|
-
* an account of its own, and nothing runs at planning time: the planner and eligibility read
|
|
11
|
-
* config only, and this module is reached from the lane's dispatch alone.
|
|
5
|
+
* Credential. Shared Anthropic routing chooses the account for the session and model;
|
|
6
|
+
* the same generation-fenced selection commit as the Responses transport admits it. A lost
|
|
7
|
+
* commit re-evaluates routing so a concurrent manual switch or policy edit wins. Selection,
|
|
8
|
+
* token resolution and affinity binding run at dispatch only, never during planning.
|
|
12
9
|
*
|
|
13
|
-
* Pools.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* lane-change 409 is distinct from the shared local authentication/pause/cooldown refusals.
|
|
10
|
+
* Pools. Native Messages uses the OAuth owner's strategy, model allowlist, session affinity,
|
|
11
|
+
* pause and cooldown admission. Pre-output refusal recovery proposes a replacement account;
|
|
12
|
+
* this module admits and binds its exact generation before a rebuilt request may send.
|
|
13
|
+
* A lane-change 409 remains distinct from typed local authentication/pause/cooldown refusals.
|
|
18
14
|
*
|
|
19
15
|
* Tool names. An OAuth request carries client tool names under the Claude OAuth prefix, as the
|
|
20
16
|
* adapter sends them; the answer's `tool_use` names are mapped back here, for exactly the names
|
|
@@ -25,12 +21,15 @@
|
|
|
25
21
|
import type { OAuthAccessSnapshot } from "../oauth";
|
|
26
22
|
import {
|
|
27
23
|
commitAnthropicSelectionRouting,
|
|
24
|
+
captureAnthropicManualSelectionGeneration,
|
|
28
25
|
getAnthropicPoolAccessSnapshot,
|
|
29
26
|
getAnthropicAccountHealthSnapshot,
|
|
30
27
|
resolveAnthropicDispatchAccountId,
|
|
31
|
-
|
|
28
|
+
resolveAnthropicAccountForSession,
|
|
29
|
+
getEligibleAnthropicAccounts,
|
|
32
30
|
isAnthropicAccountPoolEnabled,
|
|
33
31
|
} from "../oauth/anthropic-routing";
|
|
32
|
+
import { resolveAnthropicModelRoute, routeCandidates, type AnthropicRouteDecision } from "../oauth/anthropic-model-routes";
|
|
34
33
|
import {
|
|
35
34
|
captureOAuthAccountSelection,
|
|
36
35
|
commitOAuthAccountSelection,
|
|
@@ -57,9 +56,14 @@ export interface NativeOAuthBinding {
|
|
|
57
56
|
readonly snapshot: OAuthAccessSnapshot;
|
|
58
57
|
/** Anthropic account UUID from the same stored generation, distinct from snapshot.accountId. */
|
|
59
58
|
readonly providerAccountUuid?: string;
|
|
59
|
+
readonly routeDecision: AnthropicRouteDecision | null;
|
|
60
|
+
readonly sessionKey: string | null;
|
|
61
|
+
readonly model?: string;
|
|
62
|
+
readonly config: OcxConfig;
|
|
63
|
+
readonly manualSelectionGeneration: number;
|
|
60
64
|
}
|
|
61
65
|
|
|
62
|
-
/** The selection moved or became
|
|
66
|
+
/** The selection moved or became ineligible while resolving. Maps to a 409 retry. */
|
|
63
67
|
export class NativeOAuthSelectionChangedError extends Error {
|
|
64
68
|
constructor() {
|
|
65
69
|
super("OAuth account selection changed; retry the request");
|
|
@@ -67,57 +71,100 @@ export class NativeOAuthSelectionChangedError extends Error {
|
|
|
67
71
|
}
|
|
68
72
|
}
|
|
69
73
|
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
/** Invalid operator route configuration remains a request refusal, not an auth failure. */
|
|
75
|
+
export class NativeOAuthModelRouteError extends Error {}
|
|
76
|
+
|
|
77
|
+
function currentRoute(config: OcxConfig, model?: string): AnthropicRouteDecision | null {
|
|
78
|
+
const resolved = model ? resolveAnthropicModelRoute(config, model) : { decision: null };
|
|
79
|
+
if (resolved.error) throw new NativeOAuthModelRouteError(`Invalid Anthropic model routes: ${resolved.error}`);
|
|
80
|
+
return resolved.decision ? { ...resolved.decision, accounts: [...resolved.decision.accounts] } : null;
|
|
72
81
|
}
|
|
73
82
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
function routeIsCurrent(config: OcxConfig, model: string | undefined, decision: AnthropicRouteDecision | null): boolean {
|
|
84
|
+
return JSON.stringify(currentRoute(config, model)) === JSON.stringify(decision);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface NativeOAuthBindingOptions {
|
|
88
|
+
sessionKey?: string | null;
|
|
89
|
+
model?: string;
|
|
90
|
+
/** An account proposed by the shared pre-output refusal recovery policy. */
|
|
91
|
+
candidateAccountId?: string;
|
|
92
|
+
/** Selection captured before asynchronous refusal classification or throttle wait. */
|
|
93
|
+
expectedRecoverySelection?: Selection | null;
|
|
94
|
+
expectedRecoveryRouteDecision?: AnthropicRouteDecision | null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Resolve the shared pool selector and commit its exact credential generation for dispatch. */
|
|
98
|
+
export async function resolveNativeOAuthBinding(
|
|
99
|
+
config: OcxConfig,
|
|
100
|
+
options: NativeOAuthBindingOptions = {},
|
|
101
|
+
): Promise<NativeOAuthBinding> {
|
|
102
|
+
const sessionKey = options.sessionKey ?? null;
|
|
103
|
+
const model = options.model;
|
|
80
104
|
for (let attempt = 0; attempt < MAX_SELECTION_ATTEMPTS; attempt++) {
|
|
105
|
+
const routeDecision = currentRoute(config, model);
|
|
106
|
+
const manualSelectionGeneration = captureAnthropicManualSelectionGeneration();
|
|
81
107
|
const selection = captureOAuthAccountSelection(PROVIDER);
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
108
|
+
// Always ask admission first so pause/login/cooldown retains its typed local refusal.
|
|
109
|
+
const admittedId = await resolveAnthropicDispatchAccountId(config, sessionKey, routeDecision, model);
|
|
110
|
+
if (!routeIsCurrent(config, model, routeDecision)) continue;
|
|
111
|
+
const proposed = resolveAnthropicAccountForSession(sessionKey, config, Date.now(), routeDecision, model);
|
|
112
|
+
// A lost selection commit discards a recovery proposal: a newer manual/policy choice wins.
|
|
113
|
+
const recoverySelectionMatches = !!options.expectedRecoverySelection
|
|
114
|
+
&& selection?.accountId === options.expectedRecoverySelection.accountId
|
|
115
|
+
&& selection?.revision === options.expectedRecoverySelection.revision;
|
|
116
|
+
const recoveryRouteMatches = options.expectedRecoveryRouteDecision === undefined
|
|
117
|
+
|| JSON.stringify(options.expectedRecoveryRouteDecision) === JSON.stringify(routeDecision);
|
|
118
|
+
const accountId = attempt === 0 && options.candidateAccountId && recoverySelectionMatches && recoveryRouteMatches
|
|
119
|
+
? options.candidateAccountId : admittedId;
|
|
86
120
|
if (!selection) continue;
|
|
121
|
+
if (!routeCandidates(getEligibleAnthropicAccounts(Date.now(), model), routeDecision).includes(accountId)) {
|
|
122
|
+
throw new NativeOAuthSelectionChangedError();
|
|
123
|
+
}
|
|
87
124
|
const candidate = await getAnthropicPoolAccessSnapshot(accountId);
|
|
125
|
+
if (!routeIsCurrent(config, model, routeDecision)) continue;
|
|
88
126
|
const committed = await commitOAuthAccountSelection(PROVIDER, candidate.accountId, {
|
|
89
127
|
expectedSelection: selection,
|
|
90
128
|
expectedCredentialGeneration: candidate.generation,
|
|
91
129
|
requireUsableAccount: true,
|
|
92
130
|
});
|
|
93
131
|
if (committed) {
|
|
132
|
+
if (!routeIsCurrent(config, model, routeDecision)
|
|
133
|
+
|| manualSelectionGeneration !== captureAnthropicManualSelectionGeneration()) continue;
|
|
94
134
|
if (!commitAnthropicSelectionRouting(candidate.accountId, selection, committed, {
|
|
95
|
-
config,
|
|
96
|
-
|
|
135
|
+
config, sessionKey, model, routeDecision,
|
|
136
|
+
reason: accountId === proposed.accountId ? proposed.reason : undefined,
|
|
97
137
|
expectedCredentialGeneration: candidate.generation,
|
|
98
138
|
})) continue;
|
|
99
|
-
if (pooled(config)) break;
|
|
100
139
|
const row = getAccountCredentialWithStatus(PROVIDER, candidate.accountId);
|
|
101
140
|
if (!row || credentialGeneration(row.credential) !== candidate.generation) continue;
|
|
102
|
-
const binding = { selection: committed, snapshot: candidate, providerAccountUuid: row.credential.accountId };
|
|
141
|
+
const binding = { selection: committed, snapshot: candidate, providerAccountUuid: row.credential.accountId, routeDecision, sessionKey, model, config, manualSelectionGeneration };
|
|
103
142
|
if (nativeOAuthBindingIsCurrent(binding)) return binding;
|
|
104
143
|
}
|
|
105
|
-
// Re-read after a lost commit or a health change during credential resolution.
|
|
106
144
|
}
|
|
107
145
|
throw new NativeOAuthSelectionChangedError();
|
|
108
146
|
}
|
|
109
147
|
|
|
110
148
|
/**
|
|
111
|
-
* Whether a binding may still be sent:
|
|
112
|
-
*
|
|
149
|
+
* Whether a binding may still be sent: a live pooled session affinity or the same committed
|
|
150
|
+
* selection, plus the same usable credential generation. Checked before every physical send.
|
|
113
151
|
*/
|
|
114
152
|
export function nativeOAuthBindingIsCurrent(binding: NativeOAuthBinding): boolean {
|
|
153
|
+
if (binding.manualSelectionGeneration !== captureAnthropicManualSelectionGeneration()
|
|
154
|
+
|| !routeIsCurrent(binding.config, binding.model, binding.routeDecision)) return false;
|
|
115
155
|
const selected = captureOAuthAccountSelection(PROVIDER);
|
|
116
156
|
const row = getAccountCredentialWithStatus(PROVIDER, binding.snapshot.accountId);
|
|
117
|
-
|
|
118
|
-
|
|
157
|
+
const sessionRoute = isAnthropicAccountPoolEnabled(binding.config) && binding.sessionKey
|
|
158
|
+
? resolveAnthropicAccountForSession(binding.sessionKey, binding.config, Date.now(), binding.routeDecision, binding.model) : null;
|
|
159
|
+
// Another conversation may move the automatic active pointer without revoking this
|
|
160
|
+
// session's affinity. Manual selection clears affinity and precedes it in the selector.
|
|
161
|
+
const selectionCurrent = sessionRoute?.reason === "affinity"
|
|
162
|
+
&& sessionRoute.accountId === binding.snapshot.accountId
|
|
163
|
+
|| (selected?.accountId === binding.selection.accountId && selected?.revision === binding.selection.revision);
|
|
164
|
+
return selectionCurrent
|
|
119
165
|
&& !!row && !row.paused && !row.needsReauth && row.credential.expires > Date.now()
|
|
120
166
|
&& !getAnthropicAccountHealthSnapshot(binding.snapshot.accountId)
|
|
167
|
+
&& routeCandidates(getEligibleAnthropicAccounts(Date.now(), binding.model), binding.routeDecision).includes(binding.snapshot.accountId)
|
|
121
168
|
&& credentialGeneration(row.credential) === binding.snapshot.generation
|
|
122
169
|
&& row.credential.accountId === binding.providerAccountUuid;
|
|
123
170
|
}
|