@bitkyc08/opencodex 2.80.0 → 2.82.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/README.md +3 -3
- package/bin/ocx.mjs +166 -73
- package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
- package/gui/dist/assets/Tray-BqHF5Trd.js +1 -0
- package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
- package/gui/dist/assets/index-D3FBCNiP.js +94 -0
- package/gui/dist/assets/quota-summary-DTBC8n7-.js +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/claude-green.svg +1 -0
- package/package.json +3 -2
- package/src/adapters/anthropic-model-contract.ts +8 -2
- package/src/adapters/anthropic.ts +8 -3
- package/src/adapters/codebuddy/mcp-server.ts +2 -25
- package/src/adapters/codebuddy/tool-bridge.ts +1 -7
- package/src/adapters/coding-agent/mcp-server.ts +33 -0
- package/src/adapters/coding-agent/protocol.ts +122 -17
- package/src/adapters/coding-agent/tool-catalog.ts +75 -0
- package/src/adapters/coding-agent/turn.ts +101 -44
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +2 -0
- package/src/adapters/devin/live-models.ts +2 -0
- package/src/adapters/kiro-retry.ts +9 -4
- package/src/adapters/openai-chat/messages.ts +7 -6
- package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
- package/src/adapters/openai-responses/passthrough.ts +9 -4
- package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
- package/src/adapters/physical-send.ts +8 -5
- package/src/adapters/qoder/adapter.ts +41 -6
- package/src/adapters/qoder/scaffold-guard.ts +2 -3
- package/src/adapters/zed.ts +16 -1
- package/src/claude/agents-inject.ts +14 -3
- package/src/claude/context-windows.ts +19 -17
- package/src/claude/desktop-3p.ts +88 -12
- package/src/claude/desktop-profile.ts +2 -1
- package/src/claude/gateway-cache.ts +18 -1
- package/src/claude/inbound-cache-stabilize.ts +4 -9
- package/src/claude/inbound.ts +78 -3
- package/src/claude/intercept/cli-catalog.ts +3 -0
- package/src/claude/intercept/model-bindings.ts +1 -0
- package/src/claude/intercept/picker-ca-persistence.ts +7 -6
- package/src/claude/intercept/picker-listener.ts +71 -4
- package/src/claude/intercept/picker-models.ts +14 -6
- package/src/claude/intercept/settings.ts +3 -0
- package/src/claude/long-context.ts +66 -0
- package/src/claude/model-info.ts +19 -16
- package/src/claude/outbound.ts +94 -9
- package/src/claude/subagent-model.ts +40 -11
- package/src/cli/account-anthropic-threshold.ts +14 -5
- package/src/cli/account-api.ts +3 -1
- package/src/cli/account-extended.ts +13 -9
- package/src/cli/account-policy-dto.ts +1 -1
- package/src/cli/account-policy.ts +13 -4
- package/src/cli/account.ts +3 -3
- package/src/cli/agent.ts +9 -5
- package/src/cli/aside-profile-recovery.ts +33 -0
- package/src/cli/capabilities-accounts.ts +2 -2
- package/src/cli/capabilities-agents-routing.ts +24 -0
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-integrations.ts +18 -2
- package/src/cli/cli-path-diagnostics.ts +132 -0
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/desktop-runtime-guidance.ts +65 -0
- package/src/cli/dispatch.ts +15 -1
- package/src/cli/doctor.ts +16 -3
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +25 -14
- package/src/cli/integration-aside-sync.ts +2 -1
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/integration-preview.ts +8 -1
- package/src/cli/integrations.ts +48 -6
- package/src/cli/message-args.ts +34 -0
- package/src/cli/message-command.ts +43 -0
- package/src/cli/message-runtime.ts +6 -0
- package/src/cli/observe.ts +3 -1
- package/src/cli/provider.ts +29 -6
- package/src/cli/registry.ts +24 -2
- package/src/cli/resolve.ts +25 -2
- package/src/cli/root.ts +4 -2
- package/src/cli/status.ts +64 -12
- package/src/cli/update-restart-child.ts +4 -2
- package/src/cli/update-restart-home.ts +28 -10
- package/src/cli/update-restart-service-record.ts +116 -0
- package/src/cli/update-restart-supervision.ts +96 -0
- package/src/cli/update-restart.ts +59 -13
- package/src/cli/v2.ts +27 -20
- package/src/cli/version-skew-notice.ts +77 -0
- package/src/clients/config-export/commandcode.ts +131 -0
- package/src/clients/config-export/contracts.ts +12 -0
- package/src/clients/config-export.ts +64 -1
- package/src/clients/lazycodex.ts +1 -1
- package/src/clients/omo-role-models.ts +32 -7
- package/src/codex/auth-api/account-list.ts +11 -3
- package/src/codex/auth-api/account-selection.ts +107 -0
- package/src/codex/auth-api/main-account-probe.ts +25 -8
- package/src/codex/auth-api/pool-mode-gate.ts +33 -9
- package/src/codex/auth-api/pool-quota-probe.ts +15 -1
- package/src/codex/auth-api/routes.ts +11 -49
- package/src/codex/auth-context.ts +20 -1
- package/src/codex/autostart-health.ts +12 -3
- package/src/codex/catalog/auto-review.ts +13 -6
- package/src/codex/catalog/build-entries.ts +33 -7
- package/src/codex/catalog/bundled.ts +155 -26
- package/src/codex/catalog/combo-member.ts +3 -2
- package/src/codex/catalog/control-plane.ts +45 -0
- package/src/codex/catalog/effort.ts +37 -6
- package/src/codex/catalog/gather-capture.ts +10 -3
- package/src/codex/catalog/model-hints.ts +15 -8
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/operator-tiers.ts +38 -0
- package/src/codex/catalog/parsing.ts +2 -1
- package/src/codex/catalog/provider-models.ts +58 -44
- package/src/codex/catalog/retained-sync.ts +7 -5
- package/src/codex/catalog/routed-gather.ts +15 -1
- package/src/codex/catalog-auto-refresh-sources.ts +3 -4
- package/src/codex/catalog-auto-refresh.ts +19 -0
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-home-owner.ts +3 -0
- package/src/codex/config-write-lock.ts +333 -0
- package/src/codex/control-plane-models.ts +6 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/features.ts +257 -200
- package/src/codex/inject/config-write-section.ts +34 -0
- package/src/codex/inject/multi-agent-v2.ts +14 -6
- package/src/codex/inject/remove.ts +36 -11
- package/src/codex/inject/restore.ts +85 -20
- package/src/codex/inject-coordination.ts +15 -6
- package/src/codex/inject.ts +62 -20
- package/src/codex/journal.ts +62 -17
- package/src/codex/main-account-cache.ts +43 -0
- package/src/codex/paths.ts +2 -0
- package/src/codex/prepared-home.ts +18 -0
- package/src/codex/prompt-journal.ts +25 -12
- package/src/codex/prompt-layers.ts +49 -26
- package/src/codex/prompt-lock-claim.ts +134 -0
- package/src/codex/prompt-lock-evidence.ts +26 -0
- package/src/codex/prompt-lock-io.ts +16 -0
- package/src/codex/prompt-lock-owner.ts +130 -0
- package/src/codex/prompt-lock.ts +84 -53
- package/src/codex/routing/failure-window.ts +146 -0
- package/src/codex/routing/selection.ts +4 -1
- package/src/codex/routing.ts +13 -17
- package/src/codex/runtime.ts +106 -18
- package/src/codex/shim.ts +45 -19
- package/src/combos/jev-decision-contract.ts +25 -2
- package/src/combos/jev-service-exchange.ts +281 -0
- package/src/combos/jev.ts +34 -219
- package/src/combos/types.ts +1 -1
- package/src/config/atomic-write.ts +1 -1
- package/src/config/codex-ws-reuse-setting.ts +10 -0
- package/src/config/diagnostics.ts +27 -16
- package/src/config/live-reconcile.ts +17 -0
- package/src/config/load-degrade.ts +42 -0
- package/src/config/rebase-provenance.ts +11 -0
- package/src/config/schema/anthropic-account-pool.ts +143 -0
- package/src/config/schema/config-schema.ts +11 -1
- package/src/config/schema/leaf-validators.ts +4 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +566 -298
- package/src/generated/model-metadata.ts +7 -6
- package/src/grok/inject.ts +50 -4
- package/src/grok/sync.ts +2 -1
- package/src/images/loop.ts +30 -11
- package/src/integrations/aside-profiles.ts +10 -3
- package/src/integrations/catalog-refresh.ts +19 -3
- package/src/integrations/mutation-plan.ts +7 -0
- package/src/integrations/registry.ts +8 -0
- package/src/integrations/state.ts +9 -2
- package/src/integrations/writer.ts +17 -5
- package/src/lib/bun-path-runtime.d.mts +32 -0
- package/src/lib/bun-path-runtime.mjs +95 -0
- package/src/lib/bun-runtime-preflight.ts +140 -0
- package/src/lib/desktop-cli-handoff.d.mts +20 -0
- package/src/lib/desktop-cli-handoff.mjs +92 -0
- package/src/lib/desktop-cli-record.d.mts +29 -0
- package/src/lib/desktop-cli-record.mjs +102 -0
- package/src/lib/lab-live-route-production.ts +9 -1
- package/src/lib/request-execution-budget.ts +244 -58
- package/src/lib/spend-pool-alias-validation.ts +79 -0
- package/src/lib/spend-pool-continuity.ts +34 -0
- package/src/lib/spend-reservation-ledger.ts +412 -50
- package/src/lib/state-store-registrations.ts +20 -3
- package/src/lib/synced-state-location.ts +46 -6
- package/src/lib/test-home-guard.ts +87 -1
- package/src/lib/translator-budget.ts +17 -4
- package/src/lib/upstream-retry.ts +17 -5
- package/src/lib/windows-atomic-replace.ts +38 -16
- package/src/lib/windows-elevation.ts +6 -0
- package/src/lib/windows-secret-acl.ts +92 -25
- package/src/lib/winsw.ts +2 -3
- package/src/lib/workflow-budget.ts +53 -11
- package/src/messaging/budget.ts +45 -0
- package/src/messaging/discovery.ts +57 -0
- package/src/messaging/envelope.ts +49 -0
- package/src/messaging/input.ts +37 -0
- package/src/messaging/rpc.ts +196 -0
- package/src/messaging/send.ts +58 -0
- package/src/messaging/socket.ts +72 -0
- package/src/messaging/types.ts +29 -0
- package/src/oauth/anthropic-account-refusal.ts +90 -18
- package/src/oauth/anthropic-account-threshold.ts +8 -2
- package/src/oauth/anthropic-continuity.ts +15 -1
- package/src/oauth/anthropic-model-quota.ts +120 -48
- package/src/oauth/anthropic-model-routes.ts +11 -3
- package/src/oauth/anthropic-oauth-definitions.ts +98 -0
- package/src/oauth/anthropic-pool-config.ts +30 -0
- package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
- package/src/oauth/anthropic-routing.ts +1202 -866
- package/src/oauth/anthropic-send-ownership.ts +41 -0
- package/src/oauth/anthropic.ts +4 -1
- package/src/oauth/chatgpt.ts +113 -41
- package/src/oauth/generic-account-failover.ts +3 -1
- package/src/oauth/health.ts +8 -6
- package/src/oauth/index.ts +96 -175
- package/src/oauth/kiro.ts +22 -9
- package/src/oauth/login-cli.ts +2 -0
- package/src/oauth/model-discovery-auth.ts +43 -0
- package/src/oauth/pool-kernel.ts +11 -5
- package/src/oauth/pool-settings-capability.ts +5 -3
- package/src/oauth/store-anthropic-instance.ts +87 -0
- package/src/oauth/store.ts +68 -10
- package/src/oauth/token-guardian.ts +2 -0
- package/src/oauth/types.ts +2 -0
- package/src/protocols/plan-snapshot.ts +5 -0
- package/src/protocols/settings.ts +20 -5
- package/src/providers/anthropic-instance-id.ts +47 -0
- package/src/providers/anthropic-instance.ts +48 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
- package/src/providers/azure-model-metadata.ts +115 -0
- package/src/providers/derive.ts +23 -0
- package/src/providers/kiro-models.ts +3 -0
- package/src/providers/label.ts +1 -1
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/quota/account-cache.ts +37 -11
- package/src/providers/quota/anthropic-account-quota.ts +93 -0
- package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
- package/src/providers/quota/report-cache.ts +24 -1
- package/src/providers/quota/vendor-probes-key.ts +127 -17
- package/src/providers/quota/vendor-probes-oauth.ts +46 -14
- package/src/providers/quota.ts +23 -33
- package/src/providers/registry/entries-core.ts +35 -30
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +7 -4
- package/src/providers/registry/types.ts +3 -0
- package/src/providers/registry.ts +3 -3
- package/src/responses/reasoning-envelope.ts +29 -1
- package/src/responses/reasoning-replay-cache.ts +17 -1
- package/src/responses/spill-store.ts +20 -3
- package/src/responses/state/metrics.ts +95 -0
- package/src/responses/state/snapshot-codec.ts +6 -0
- package/src/responses/state/spill-failure.ts +12 -0
- package/src/responses/state/spill-queue.ts +13 -5
- package/src/responses/state/unforced-store-false.ts +55 -0
- package/src/responses/state.ts +68 -76
- package/src/responses/task-input.ts +17 -0
- package/src/router.ts +28 -4
- package/src/routing/compatibility/assemble.ts +1 -1
- package/src/routing/quota.ts +7 -3
- package/src/server/auth-cors.ts +5 -0
- package/src/server/background-lifecycle.ts +27 -0
- package/src/server/chat-completions.ts +7 -1
- package/src/server/chat-native.ts +64 -28
- package/src/server/claude-messages.ts +59 -8
- package/src/server/gui-static.ts +38 -4
- package/src/server/inbound-body-admission.ts +1 -0
- package/src/server/index/claude-intercept-lifecycle.ts +3 -1
- package/src/server/index/serve-options.ts +10 -2
- package/src/server/index/spend-ledger-lifecycle.ts +12 -5
- package/src/server/index/startup-warnings.ts +19 -0
- package/src/server/index.ts +5 -2
- package/src/server/inference/context.ts +1 -1
- package/src/server/lifecycle.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +123 -38
- package/src/server/management/anthropic-account-threshold.ts +12 -9
- package/src/server/management/anthropic-pool-settings.ts +72 -0
- package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
- package/src/server/management/codex-agent-role-routes.ts +4 -2
- package/src/server/management/codex-prompt-routes.ts +1 -0
- package/src/server/management/codex-role-auto-assign.ts +13 -2
- package/src/server/management/config-routes.ts +85 -14
- package/src/server/management/context.ts +2 -2
- package/src/server/management/decision-routes.ts +2 -1
- package/src/server/management/integration-routes.ts +25 -2
- package/src/server/management/oauth-account-routes.ts +79 -98
- package/src/server/management/provider-instance-ownership.ts +44 -0
- package/src/server/management/provider-routes.ts +21 -28
- package/src/server/management/route-registry.ts +3 -1
- package/src/server/management/routing-profile-routes.ts +4 -4
- package/src/server/management/shared.ts +3 -3
- package/src/server/management/system-restart.ts +44 -14
- package/src/server/management/system-routes.ts +24 -2
- package/src/server/management/vision-sidecar-options.ts +9 -6
- package/src/server/management/web-search-sidecar-options.ts +51 -2
- package/src/server/management-auth.ts +6 -0
- package/src/server/messages-native-eligibility.ts +3 -2
- package/src/server/messages-native-oauth.ts +91 -45
- package/src/server/messages-native-selector.ts +39 -0
- package/src/server/messages-native.ts +133 -38
- package/src/server/messages-response-headers.ts +14 -0
- package/src/server/request-log-conversation.ts +48 -5
- package/src/server/request-log.ts +36 -2
- package/src/server/responses/adapter-continuation.ts +31 -19
- package/src/server/responses/adapter-dispatch.ts +75 -24
- package/src/server/responses/antigravity-validation-refusal.ts +20 -6
- package/src/server/responses/codex-ws-exchange.ts +17 -2
- package/src/server/responses/codex-ws-pool.ts +100 -13
- package/src/server/responses/codex-ws-wire.ts +9 -0
- package/src/server/responses/combo-requested-effort.ts +3 -1
- package/src/server/responses/compact.ts +25 -8
- package/src/server/responses/compaction-recovery.ts +1 -1
- package/src/server/responses/core-codex-account.ts +22 -1
- package/src/server/responses/core-combo-native.ts +24 -4
- package/src/server/responses/core-combo.ts +38 -25
- package/src/server/responses/core-lifetime.ts +3 -36
- package/src/server/responses/core-normalize.ts +1 -0
- package/src/server/responses/core-options.ts +8 -1
- package/src/server/responses/core-replay.ts +78 -0
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/fetch-helpers.ts +26 -3
- package/src/server/responses/passthrough-delivery.ts +30 -4
- package/src/server/responses/passthrough-dispatch.ts +39 -9
- package/src/server/responses/request-prepare.ts +41 -5
- package/src/server/responses/request-send-budget.ts +150 -50
- package/src/server/responses/request-spend.ts +325 -33
- package/src/server/responses/request-transport.ts +132 -52
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +13 -3
- package/src/server/responses/sidecar-execution.ts +113 -22
- package/src/server/responses/sidecar-send-budget.ts +58 -0
- package/src/server/responses/ws-upstream.ts +19 -6
- package/src/server/responses-undeclared-tool-guard.ts +5 -1
- package/src/server/startup-health-cache.ts +2 -1
- package/src/server/workflow-refusal.ts +33 -0
- package/src/service/cli.ts +20 -4
- package/src/service/desktop-command-guard.ts +32 -0
- package/src/service/desktop-startup.ts +66 -40
- package/src/service/desktop-supervision.d.mts +26 -0
- package/src/service/desktop-supervision.mjs +140 -0
- package/src/service/managing-cli.ts +1 -0
- package/src/service/orchestration.ts +46 -20
- package/src/service/ownership-mutation-lease.d.mts +1 -1
- package/src/service/ownership-mutation-lease.mjs +5 -7
- package/src/service/repair.ts +25 -9
- package/src/service/state.ts +4 -3
- package/src/service/systemd.ts +17 -8
- package/src/service/windows-ops.ts +61 -22
- package/src/sidecar/anthropic-binding.ts +79 -0
- package/src/sidecar/auth.ts +76 -2
- package/src/storage/policy-job.ts +5 -1
- package/src/storage/policy-worker.ts +3 -1
- package/src/storage/policy.ts +9 -0
- package/src/types/anthropic-account-pool.ts +33 -0
- package/src/types/config.ts +33 -28
- package/src/types/provider.ts +5 -0
- package/src/types/request.ts +5 -0
- package/src/types/tools.ts +7 -0
- package/src/types/wire.ts +1 -0
- package/src/types.ts +4 -0
- package/src/update/index.ts +57 -9
- package/src/update/restart-ownership.ts +10 -3
- package/src/update/runtime-ownership.d.mts +6 -0
- package/src/update/runtime-ownership.mjs +23 -7
- package/src/usage/cost.ts +3 -2
- package/src/usage/expected-prices.ts +66 -14
- package/src/vision/anthropic-describe.ts +16 -3
- package/src/vision/index.ts +3 -0
- package/src/vision/plan.ts +19 -6
- package/src/web-search/alpha-search.ts +7 -1
- package/src/web-search/anthropic-executor.ts +16 -4
- package/src/web-search/index.ts +7 -3
- package/src/web-search/loop.ts +32 -7
- package/src/web-search/passthrough-bridge.ts +4 -1
- package/src/web-search/sidecar-providers.ts +4 -3
- package/gui/dist/assets/App-MbDVfUeC.js +0 -54
- package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
- package/gui/dist/assets/index-UJt6npXk.js +0 -94
- package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
package/src/oauth/pool-kernel.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isAnthropicInstanceId, type AnthropicInstanceId } from "../providers/anthropic-instance-id";
|
|
1
2
|
import type { OcxAccountPoolRotationStrategy } from "../types";
|
|
2
3
|
import type { GenerationContext } from "../lib/state-store-sweeper";
|
|
3
4
|
|
|
@@ -277,7 +278,7 @@ export function clearPoolRotationState(poolKey?: string): void {
|
|
|
277
278
|
|
|
278
279
|
export function reconcilePoolRotationState(context: GenerationContext): number {
|
|
279
280
|
if (context.generation <= lastReconciledGeneration) return 0;
|
|
280
|
-
const anthropicIds = new Set<string
|
|
281
|
+
const anthropicIds = new Map<AnthropicInstanceId, Set<string>>();
|
|
281
282
|
// Live account ids per generic OAuth provider, built from the same
|
|
282
283
|
// `provider\0id` roster the Anthropic pass already walks. Without this the
|
|
283
284
|
// `generic:*` entries fall through as unknown and are never swept, so a removed
|
|
@@ -288,8 +289,10 @@ export function reconcilePoolRotationState(context: GenerationContext): number {
|
|
|
288
289
|
if (separator <= 0) continue;
|
|
289
290
|
const provider = key.slice(0, separator);
|
|
290
291
|
const accountId = key.slice(separator + 1);
|
|
291
|
-
if (provider
|
|
292
|
-
anthropicIds.
|
|
292
|
+
if (isAnthropicInstanceId(provider)) {
|
|
293
|
+
let ids = anthropicIds.get(provider);
|
|
294
|
+
if (!ids) { ids = new Set(); anthropicIds.set(provider, ids); }
|
|
295
|
+
ids.add(accountId);
|
|
293
296
|
continue;
|
|
294
297
|
}
|
|
295
298
|
let bucket = genericIds.get(provider);
|
|
@@ -301,8 +304,8 @@ export function reconcilePoolRotationState(context: GenerationContext): number {
|
|
|
301
304
|
}
|
|
302
305
|
let removed = 0;
|
|
303
306
|
for (const [poolKey, state] of selectionState) {
|
|
304
|
-
const valid = poolKey
|
|
305
|
-
? anthropicIds
|
|
307
|
+
const valid = isAnthropicInstanceId(poolKey)
|
|
308
|
+
? anthropicIds.get(poolKey) ?? new Set<string>()
|
|
306
309
|
: poolKey === POOL_KEY_CODEX || poolKey.startsWith(`${POOL_KEY_CODEX}:`)
|
|
307
310
|
? context.codexAccountIds
|
|
308
311
|
: poolKey.startsWith("generic:")
|
|
@@ -328,3 +331,6 @@ export function reconcilePoolRotationState(context: GenerationContext): number {
|
|
|
328
331
|
lastReconciledGeneration = context.generation;
|
|
329
332
|
return removed;
|
|
330
333
|
}
|
|
334
|
+
|
|
335
|
+
/** Separate cursor and weights for each Anthropic OAuth instance. */
|
|
336
|
+
export function anthropicPoolKey(instance: AnthropicInstanceId): string { return instance; }
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isBuiltinAnthropicInstanceRow, isAnthropicInstanceId } from "../providers/anthropic-instance";
|
|
2
|
+
import { resolveAnthropicAccountPoolConfig } from "./anthropic-pool-config";
|
|
1
3
|
import { isGenericFailoverProvider } from "./generic-account-failover";
|
|
2
4
|
import { parseAccountPoolStickyLimit, parseAccountPoolStrategy, parseCodexAccountPoolStrategy } from "./pool-kernel";
|
|
3
5
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
@@ -25,8 +27,8 @@ export type GenericPoolStrategy = typeof GENERIC_POOL_STRATEGIES[number];
|
|
|
25
27
|
|
|
26
28
|
export function poolSettingsCapability(name: string, provider: OcxProviderConfig | undefined): PoolSettingsKind | null {
|
|
27
29
|
if (name === "openai") return "codex";
|
|
28
|
-
if (name === "anthropic") return "anthropic";
|
|
29
|
-
if (!provider) return null;
|
|
30
|
+
if (name === "anthropic" || (provider?.disabled !== true && isBuiltinAnthropicInstanceRow(name, provider))) return "anthropic";
|
|
31
|
+
if (name === "anthropic2" || !provider) return null;
|
|
30
32
|
return isGenericFailoverProvider(name, provider) ? "generic" : null;
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -173,7 +175,7 @@ export function unifiedPoolSettingsDto(
|
|
|
173
175
|
};
|
|
174
176
|
}
|
|
175
177
|
if (kind === "anthropic") {
|
|
176
|
-
const pool = config
|
|
178
|
+
const pool = isAnthropicInstanceId(provider) ? resolveAnthropicAccountPoolConfig(config, provider) : {};
|
|
177
179
|
const enabled = typeof pool.enabled === "boolean" ? pool.enabled : null;
|
|
178
180
|
return {
|
|
179
181
|
...base,
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { getConfigPath } from "../config/paths";
|
|
4
|
+
import { anthropicInstanceRowShapeMatches, type AnthropicInstanceId, type AnthropicInstanceRow } from "../providers/anthropic-instance-id";
|
|
5
|
+
import type { OcxConfig } from "../types";
|
|
6
|
+
import { normalizeAnthropicIdentity } from "./anthropic-identity";
|
|
7
|
+
import type { AuthStore } from "./store";
|
|
8
|
+
import type { OAuthCredentials } from "./types";
|
|
9
|
+
|
|
10
|
+
export class AnthropicCrossInstanceDuplicateError extends Error {
|
|
11
|
+
readonly code = "ANTHROPIC_CROSS_INSTANCE_DUPLICATE";
|
|
12
|
+
constructor() {
|
|
13
|
+
super("This Anthropic credential is already registered in the other pool; use a different account or remove the existing registration first.");
|
|
14
|
+
this.name = "AnthropicCrossInstanceDuplicateError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class AnthropicLocalCliImportError extends Error {
|
|
19
|
+
readonly code = "ANTHROPIC_LOCAL_CLI_IMPORT_FORBIDDEN";
|
|
20
|
+
constructor() {
|
|
21
|
+
super("Anthropic Pool 2 supports browser OAuth only; local Claude Code credentials cannot be imported or adopted.");
|
|
22
|
+
this.name = "AnthropicLocalCliImportError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class AnthropicInstanceCollisionError extends Error {
|
|
27
|
+
readonly code = "ANTHROPIC_INSTANCE_COLLISION";
|
|
28
|
+
constructor(credentialWritten = false) {
|
|
29
|
+
super("anthropic2 is already configured as a custom provider; rename the custom provider before logging in to Pool 2."
|
|
30
|
+
+ (credentialWritten ? " The saved Pool 2 credential remains an orphan auth row; remove it or rename the custom provider before retrying." : ""));
|
|
31
|
+
this.name = "AnthropicInstanceCollisionError";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function assertAnthropicInstanceLoginConfig(config: Pick<OcxConfig, "providers">, provider: string, credentialWritten = false): void {
|
|
36
|
+
if (provider !== "anthropic2") return;
|
|
37
|
+
const row = config.providers.anthropic2;
|
|
38
|
+
if (Object.hasOwn(config.providers, provider) && !anthropicInstanceRowShapeMatches(provider, row)) {
|
|
39
|
+
throw new AnthropicInstanceCollisionError(credentialWritten);
|
|
40
|
+
}
|
|
41
|
+
// A malformed custom row (for example, missing baseUrl) can disappear behind loadConfig's
|
|
42
|
+
// fallback. Raw presence still owns this namespace; onboarding must not overwrite its bytes.
|
|
43
|
+
let raw: string;
|
|
44
|
+
try { raw = readFileSync(getConfigPath(), "utf8"); }
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return;
|
|
47
|
+
throw new Error("Cannot verify Anthropic Pool 2 ownership in the existing config file.");
|
|
48
|
+
}
|
|
49
|
+
let parsed: unknown;
|
|
50
|
+
try { parsed = JSON.parse(raw.replace(/^\uFEFF/, "")); }
|
|
51
|
+
catch { throw new Error("Cannot verify Anthropic Pool 2 ownership in the existing config file."); }
|
|
52
|
+
const providers = parsed && typeof parsed === "object" && "providers" in parsed ? parsed.providers : undefined;
|
|
53
|
+
if (providers && typeof providers === "object" && Object.hasOwn(providers, provider)) {
|
|
54
|
+
const persisted = (providers as Record<string, unknown>)[provider];
|
|
55
|
+
if (!persisted || typeof persisted !== "object" || Array.isArray(persisted)
|
|
56
|
+
|| !anthropicInstanceRowShapeMatches(provider, persisted as AnthropicInstanceRow)) {
|
|
57
|
+
throw new AnthropicInstanceCollisionError(credentialWritten);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function assertAnthropicCredentialSource(instance: AnthropicInstanceId, credential: OAuthCredentials): void {
|
|
63
|
+
if (instance === "anthropic2" && credential.source === "local-cli") throw new AnthropicLocalCliImportError();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function tokenFingerprint(token: string): string | undefined {
|
|
67
|
+
return token.length > 0 ? createHash("sha256").update(token).digest("hex") : undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Registration only, called under the auth-store writer lock before any row changes. */
|
|
71
|
+
export function assertNoCrossAnthropicRegistration(store: AuthStore, instance: AnthropicInstanceId, credential: OAuthCredentials): void {
|
|
72
|
+
assertAnthropicCredentialSource(instance, credential);
|
|
73
|
+
const other = instance === "anthropic" ? "anthropic2" : "anthropic";
|
|
74
|
+
const tokens = new Set([tokenFingerprint(credential.access), tokenFingerprint(credential.refresh)]
|
|
75
|
+
.filter((fingerprint): fingerprint is string => fingerprint !== undefined));
|
|
76
|
+
const identity = normalizeAnthropicIdentity(credential.anthropicIdentity, credential.access);
|
|
77
|
+
// Paused, needs-reauth and orphan rows retain ownership; labels are never proof.
|
|
78
|
+
for (const row of store[other]?.accounts ?? []) {
|
|
79
|
+
const access = tokenFingerprint(row.credential.access);
|
|
80
|
+
const refresh = tokenFingerprint(row.credential.refresh);
|
|
81
|
+
const existingIdentity = normalizeAnthropicIdentity(row.credential.anthropicIdentity, row.credential.access);
|
|
82
|
+
if ((access !== undefined && tokens.has(access)) || (refresh !== undefined && tokens.has(refresh))
|
|
83
|
+
|| (identity && existingIdentity && identity.accountUuid === existingIdentity.accountUuid)) {
|
|
84
|
+
throw new AnthropicCrossInstanceDuplicateError();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
package/src/oauth/store.ts
CHANGED
|
@@ -20,10 +20,12 @@
|
|
|
20
20
|
* both append distinct identified accounts under multiauth.
|
|
21
21
|
*/
|
|
22
22
|
import { normalizeAnthropicIdentity, matchesAnthropicImport, mergeAnthropicIdentity } from "./anthropic-identity";
|
|
23
|
+
import { isAnthropicInstanceId, type AnthropicInstanceId } from "../providers/anthropic-instance-id";
|
|
24
|
+
import { assertNoCrossAnthropicRegistration, assertAnthropicCredentialSource, assertAnthropicInstanceLoginConfig } from "./store-anthropic-instance";
|
|
23
25
|
import { createHash, randomUUID } from "node:crypto";
|
|
24
26
|
import { chmodSync, closeSync, constants as fsConstants, copyFileSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
25
27
|
import { join } from "node:path";
|
|
26
|
-
import { getConfigDir, atomicWriteFile, backupInvalidConfig, hardenConfigDir, hardenExistingSecret, withConfigMutationLockSync } from "../config";
|
|
28
|
+
import { getConfigDir, atomicWriteFile, backupInvalidConfig, hardenConfigDir, hardenExistingSecret, withConfigMutationLockSync, loadConfig } from "../config";
|
|
27
29
|
import { atomicWriteFileNoFollowUnclaimed } from "../config/atomic-write";
|
|
28
30
|
import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
|
|
29
31
|
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
@@ -630,6 +632,9 @@ function normalizeAccount(value: unknown): ProviderAccount | null {
|
|
|
630
632
|
const account: ProviderAccount = { id: candidate.id, credential };
|
|
631
633
|
if (typeof candidate.alias === "string" && candidate.alias.trim()) account.alias = candidate.alias.trim();
|
|
632
634
|
if (candidate.needsReauth === true) account.needsReauth = true;
|
|
635
|
+
if (candidate.refreshAttentionGeneration === credentialGeneration(credential)) {
|
|
636
|
+
account.refreshAttentionGeneration = candidate.refreshAttentionGeneration;
|
|
637
|
+
}
|
|
633
638
|
if (candidate.paused === true) account.paused = true;
|
|
634
639
|
const threshold = parseAnthropicAccountThreshold(candidate.autoSwitchThresholdOverride);
|
|
635
640
|
if (threshold !== null) account.autoSwitchThresholdOverride = threshold;
|
|
@@ -862,14 +867,26 @@ export interface OAuthCredentialWriteReceipt {
|
|
|
862
867
|
previousAccount: ProviderAccount | undefined;
|
|
863
868
|
}
|
|
864
869
|
|
|
870
|
+
/** Recheck B immediately before persist, including writes queued behind another mutation. */
|
|
871
|
+
function registrationPersistGuard(provider: string, assertBeforePersist?: () => void): (() => void) | undefined {
|
|
872
|
+
if (provider !== "anthropic2") return assertBeforePersist;
|
|
873
|
+
return () => {
|
|
874
|
+
assertBeforePersist?.();
|
|
875
|
+
assertAnthropicInstanceLoginConfig(loadConfig(), provider);
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
|
|
865
879
|
export async function saveCredentialWithReceipt(
|
|
866
880
|
provider: string,
|
|
867
881
|
cred: OAuthCredentials,
|
|
868
882
|
opts: { preserveIdentityless?: boolean; assertBeforePersist?: () => void } = {},
|
|
869
883
|
): Promise<OAuthCredentialWriteReceipt | null> {
|
|
884
|
+
if (isAnthropicInstanceId(provider)) assertAnthropicCredentialSource(provider, cred);
|
|
870
885
|
const safe = normalizeCredential(cred);
|
|
871
886
|
if (!safe) return null;
|
|
872
887
|
const receipt = await mutateStore(store => {
|
|
888
|
+
if (provider === "anthropic2") assertAnthropicInstanceLoginConfig(loadConfig(), provider);
|
|
889
|
+
if (isAnthropicInstanceId(provider)) assertNoCrossAnthropicRegistration(store, provider, safe);
|
|
873
890
|
const set = store[provider];
|
|
874
891
|
const previousActiveAccountId = set?.activeAccountId;
|
|
875
892
|
const previousSelectionRevision = set?.selectionRevision;
|
|
@@ -892,6 +909,7 @@ export async function saveCredentialWithReceipt(
|
|
|
892
909
|
set.selectionRevision = previousSelectionRevision;
|
|
893
910
|
delete existing.needsReauth;
|
|
894
911
|
delete existing.needsReauthReason;
|
|
912
|
+
delete existing.refreshAttentionGeneration;
|
|
895
913
|
accountId = existing.id;
|
|
896
914
|
} else {
|
|
897
915
|
const id = distinctAccountId(safe, set.accounts);
|
|
@@ -905,6 +923,7 @@ export async function saveCredentialWithReceipt(
|
|
|
905
923
|
existing.credential = safe;
|
|
906
924
|
delete existing.needsReauth;
|
|
907
925
|
delete existing.needsReauthReason;
|
|
926
|
+
delete existing.refreshAttentionGeneration;
|
|
908
927
|
if (existing.paused !== true) set.activeAccountId = existing.id;
|
|
909
928
|
accountId = existing.id;
|
|
910
929
|
} else {
|
|
@@ -917,6 +936,7 @@ export async function saveCredentialWithReceipt(
|
|
|
917
936
|
active.credential = safe;
|
|
918
937
|
delete active.needsReauth;
|
|
919
938
|
delete active.needsReauthReason;
|
|
939
|
+
delete active.refreshAttentionGeneration;
|
|
920
940
|
accountId = active.id;
|
|
921
941
|
} else {
|
|
922
942
|
const id = distinctAccountId(safe, set.accounts);
|
|
@@ -937,6 +957,7 @@ export async function saveCredentialWithReceipt(
|
|
|
937
957
|
active.credential = safe;
|
|
938
958
|
delete active.needsReauth;
|
|
939
959
|
delete active.needsReauthReason;
|
|
960
|
+
delete active.refreshAttentionGeneration;
|
|
940
961
|
accountId = active.id;
|
|
941
962
|
} else {
|
|
942
963
|
const id = distinctAccountId(safe, set.accounts);
|
|
@@ -965,7 +986,7 @@ export async function saveCredentialWithReceipt(
|
|
|
965
986
|
previousAccount: previousAccounts.get(accountId),
|
|
966
987
|
};
|
|
967
988
|
}, [provider, safe], {
|
|
968
|
-
assertBeforePersist: opts.assertBeforePersist,
|
|
989
|
+
assertBeforePersist: registrationPersistGuard(provider, opts.assertBeforePersist),
|
|
969
990
|
finalizeResult: (receipt, store) => {
|
|
970
991
|
receipt.selectionRevision = store[provider]?.selectionRevision;
|
|
971
992
|
},
|
|
@@ -1064,11 +1085,14 @@ export async function upsertCredentialByIdentity(
|
|
|
1064
1085
|
provider: string,
|
|
1065
1086
|
cred: OAuthCredentials,
|
|
1066
1087
|
): Promise<"inserted" | "updated"> {
|
|
1088
|
+
if (isAnthropicInstanceId(provider)) assertAnthropicCredentialSource(provider, cred);
|
|
1067
1089
|
const safe = normalizeCredential(cred);
|
|
1068
1090
|
if (!safe || (!safe.accountId && !safe.email)) {
|
|
1069
1091
|
throw new Error("Refusing to persist OAuth credential without verified identity");
|
|
1070
1092
|
}
|
|
1071
1093
|
return await mutateStore(store => {
|
|
1094
|
+
if (provider === "anthropic2") assertAnthropicInstanceLoginConfig(loadConfig(), provider);
|
|
1095
|
+
if (isAnthropicInstanceId(provider)) assertNoCrossAnthropicRegistration(store, provider, safe);
|
|
1072
1096
|
const set = store[provider];
|
|
1073
1097
|
const matchesEmailOnly = (account: ProviderAccount): boolean => {
|
|
1074
1098
|
if (account.credential.accountId) return false;
|
|
@@ -1086,6 +1110,7 @@ export async function upsertCredentialByIdentity(
|
|
|
1086
1110
|
existing.credential = safe;
|
|
1087
1111
|
delete existing.needsReauth;
|
|
1088
1112
|
delete existing.needsReauthReason;
|
|
1113
|
+
delete existing.refreshAttentionGeneration;
|
|
1089
1114
|
set.activeAccountId ??= existing.id;
|
|
1090
1115
|
return "updated";
|
|
1091
1116
|
}
|
|
@@ -1098,7 +1123,7 @@ export async function upsertCredentialByIdentity(
|
|
|
1098
1123
|
store[provider] = { activeAccountId: id, accounts: [account] };
|
|
1099
1124
|
}
|
|
1100
1125
|
return "inserted";
|
|
1101
|
-
}, [provider, safe]);
|
|
1126
|
+
}, [provider, safe], { assertBeforePersist: registrationPersistGuard(provider) });
|
|
1102
1127
|
}
|
|
1103
1128
|
|
|
1104
1129
|
/**
|
|
@@ -1172,16 +1197,19 @@ export function getAccountCredentialWithStatus(
|
|
|
1172
1197
|
};
|
|
1173
1198
|
}
|
|
1174
1199
|
|
|
1175
|
-
/** Persist
|
|
1200
|
+
/** Persist an explicit login/reauth credential for one account; runtime rotations use mergeAccountCredential. */
|
|
1176
1201
|
export async function saveAccountCredential(
|
|
1177
1202
|
provider: string,
|
|
1178
1203
|
accountId: string,
|
|
1179
1204
|
cred: OAuthCredentials,
|
|
1180
1205
|
opts: { assertBeforePersist?: () => void; rotateLoginId?: boolean } = {},
|
|
1181
1206
|
): Promise<void> {
|
|
1207
|
+
if (isAnthropicInstanceId(provider)) assertAnthropicCredentialSource(provider, cred);
|
|
1182
1208
|
const safe = normalizeCredential(cred);
|
|
1183
1209
|
if (!safe) return;
|
|
1184
1210
|
await mutateStore(store => {
|
|
1211
|
+
if (provider === "anthropic2") assertAnthropicInstanceLoginConfig(loadConfig(), provider);
|
|
1212
|
+
if (isAnthropicInstanceId(provider)) assertNoCrossAnthropicRegistration(store, provider, safe);
|
|
1185
1213
|
const set = store[provider];
|
|
1186
1214
|
const account = set?.accounts.find(a => a.id === accountId);
|
|
1187
1215
|
if (!set || !account) return;
|
|
@@ -1189,6 +1217,7 @@ export async function saveAccountCredential(
|
|
|
1189
1217
|
if (opts.rotateLoginId) account.loginId = randomUUID();
|
|
1190
1218
|
delete account.needsReauth;
|
|
1191
1219
|
delete account.needsReauthReason;
|
|
1220
|
+
delete account.refreshAttentionGeneration;
|
|
1192
1221
|
// A pause that found no usable fallback leaves the active id on a paused row. Once this
|
|
1193
1222
|
// reauthentication makes an unpaused account usable, hand the selection to it.
|
|
1194
1223
|
const active = set.accounts.find(a => a.id === set.activeAccountId);
|
|
@@ -1196,7 +1225,7 @@ export async function saveAccountCredential(
|
|
|
1196
1225
|
set.activeAccountId = account.id;
|
|
1197
1226
|
set.selectionRevision = randomUUID();
|
|
1198
1227
|
}
|
|
1199
|
-
}, [provider, accountId, safe], { assertBeforePersist: opts.assertBeforePersist });
|
|
1228
|
+
}, [provider, accountId, safe], { assertBeforePersist: registrationPersistGuard(provider, opts.assertBeforePersist) });
|
|
1200
1229
|
}
|
|
1201
1230
|
|
|
1202
1231
|
function accountSelection(set: ProviderAccountSet): OAuthAccountSelection {
|
|
@@ -1275,13 +1304,20 @@ export async function setAnthropicAccountThreshold(
|
|
|
1275
1304
|
accountId: string,
|
|
1276
1305
|
threshold: number | null,
|
|
1277
1306
|
options: { assertBeforePersist?: () => void } = {},
|
|
1307
|
+
): Promise<boolean> {
|
|
1308
|
+
return setAnthropicAccountThresholdForInstance("anthropic", accountId, threshold, options);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
export async function setAnthropicAccountThresholdForInstance(
|
|
1312
|
+
instance: AnthropicInstanceId, accountId: string, threshold: number | null,
|
|
1313
|
+
options: { assertBeforePersist?: () => void } = {},
|
|
1278
1314
|
): Promise<boolean> {
|
|
1279
1315
|
const normalizedThreshold = threshold === null ? null : parseAnthropicAccountThreshold(threshold);
|
|
1280
1316
|
if (threshold !== null && normalizedThreshold === null) {
|
|
1281
1317
|
throw new Error("threshold must be an integer 0-100 or null");
|
|
1282
1318
|
}
|
|
1283
1319
|
const result = await mutateStore(store => {
|
|
1284
|
-
const set = store
|
|
1320
|
+
const set = store[instance];
|
|
1285
1321
|
const account = set?.accounts.find(row => row.id === accountId);
|
|
1286
1322
|
if (!set || !account) return { status: "not-found" as const };
|
|
1287
1323
|
if ((account.autoSwitchThresholdOverride ?? null) === normalizedThreshold) return { status: "unchanged" as const };
|
|
@@ -1290,12 +1326,12 @@ export async function setAnthropicAccountThreshold(
|
|
|
1290
1326
|
else account.autoSwitchThresholdOverride = normalizedThreshold;
|
|
1291
1327
|
set.selectionRevision = randomUUID();
|
|
1292
1328
|
return { status: "updated" as const, before, after: accountSelection(set) };
|
|
1293
|
-
}, [accountId, normalizedThreshold], { assertBeforePersist: options.assertBeforePersist, afterPersist: result => {
|
|
1329
|
+
}, [instance, accountId, normalizedThreshold], { assertBeforePersist: options.assertBeforePersist, afterPersist: result => {
|
|
1294
1330
|
if (result.status !== "updated") return;
|
|
1295
1331
|
// Publish the policy-owned transition before the generic selection event. This
|
|
1296
1332
|
// preserves an exact previous-revision manual intent without opening an ABA gap.
|
|
1297
1333
|
publishOAuthAccountRoutingPolicyChange(Object.freeze({
|
|
1298
|
-
provider:
|
|
1334
|
+
provider: instance,
|
|
1299
1335
|
before: Object.freeze(result.before),
|
|
1300
1336
|
after: Object.freeze(result.after),
|
|
1301
1337
|
}));
|
|
@@ -1403,6 +1439,7 @@ export async function replaceProviderAccountSet(
|
|
|
1403
1439
|
credential: { ...account.credential, ...(account.credential.kiro ? { kiro: { ...account.credential.kiro } } : {}) },
|
|
1404
1440
|
...(account.alias ? { alias: account.alias } : {}),
|
|
1405
1441
|
...(account.needsReauth ? { needsReauth: true } : {}),
|
|
1442
|
+
...(account.refreshAttentionGeneration ? { refreshAttentionGeneration: account.refreshAttentionGeneration } : {}),
|
|
1406
1443
|
...(account.paused ? { paused: true } : {}),
|
|
1407
1444
|
...(account.autoSwitchThresholdOverride !== undefined ? { autoSwitchThresholdOverride: account.autoSwitchThresholdOverride } : {}),
|
|
1408
1445
|
...(account.needsReauthReason === "verify_account" ? { needsReauthReason: account.needsReauthReason } : {}),
|
|
@@ -1468,7 +1505,28 @@ export async function markAccountNeedsReauth(
|
|
|
1468
1505
|
}, [provider, accountId]);
|
|
1469
1506
|
}
|
|
1470
1507
|
|
|
1471
|
-
export async function mergeAccountCredential(provider:string,accountId:string,credential:OAuthCredentials,opts:{expectedGeneration?:string;afterPrePersistRead?:()=>void|Promise<void>;assertOwnership?: (store: AuthStore) => void}={}):Promise<{superseded:false}|{superseded:true;stored:OAuthCredentials}>{const safe=normalizeCredential(credential);if(!safe)throw new Error("Refusing to persist invalid OAuth credential");return await mutateStore(async store=>{await opts.afterPrePersistRead?.();const account=store[provider]?.accounts.find(x=>x.id===accountId);if(!account)throw new Error(`OAuth account disappeared before persist: ${provider}`);if(opts.expectedGeneration!==undefined&&credentialGeneration(account.credential)!==opts.expectedGeneration)return{superseded:true,stored:account.credential};opts.assertOwnership?.(store);account.credential=safe;if(account.needsReauthReason!=="verify_account"){delete account.needsReauth;delete account.needsReauthReason;}return{superseded:false};},[provider,accountId,safe,opts.expectedGeneration]);}
|
|
1508
|
+
export async function mergeAccountCredential(provider:string,accountId:string,credential:OAuthCredentials,opts:{expectedGeneration?:string;afterPrePersistRead?:()=>void|Promise<void>;assertOwnership?: (store: AuthStore) => void}={}):Promise<{superseded:false}|{superseded:true;stored:OAuthCredentials}>{const safe=normalizeCredential(credential);if(!safe)throw new Error("Refusing to persist invalid OAuth credential");if(isAnthropicInstanceId(provider))assertAnthropicCredentialSource(provider,safe);return await mutateStore(async store=>{await opts.afterPrePersistRead?.();const account=store[provider]?.accounts.find(x=>x.id===accountId);if(!account)throw new Error(`OAuth account disappeared before persist: ${provider}`);if(opts.expectedGeneration!==undefined&&credentialGeneration(account.credential)!==opts.expectedGeneration)return{superseded:true,stored:account.credential};opts.assertOwnership?.(store);account.credential=safe;delete account.refreshAttentionGeneration;if(account.needsReauthReason!=="verify_account"){delete account.needsReauth;delete account.needsReauthReason;}return{superseded:false};},[provider,accountId,safe,opts.expectedGeneration]);}
|
|
1472
1509
|
// A late refresh failure must not change an operator-paused account's health. Check under
|
|
1473
1510
|
// the mutation lock, not before awaiting it, so a concurrent pause cannot be overwritten.
|
|
1474
|
-
export async function markAccountNeedsReauthIfGeneration(provider:string,accountId:string,generation:string,writerGeneration=captureConfigGeneration(),reason?:"verify_account"):Promise<boolean>{const key=oauthAccountKey(provider,accountId);if(writerGeneration<lastReconciledGeneration&&!liveOAuthAccountKeys.has(key))return false;return await mutateStore(store=>{const account=store[provider]?.accounts.find(x=>x.id===accountId);if(!account?.credential||account.paused||credentialGeneration(account.credential)!==generation)return false;if(writerGeneration<lastReconciledGeneration&&!liveOAuthAccountKeys.has(key))return false;account.needsReauth=true;if(reason==="verify_account")account.needsReauthReason=reason;return true;},[provider,accountId,generation]);}
|
|
1511
|
+
export async function markAccountNeedsReauthIfGeneration(provider:string,accountId:string,generation:string,writerGeneration=captureConfigGeneration(),reason?:"verify_account",isCurrent?:(store:AuthStore)=>boolean):Promise<boolean>{const key=oauthAccountKey(provider,accountId);if(writerGeneration<lastReconciledGeneration&&!liveOAuthAccountKeys.has(key))return false;return await mutateStore(store=>{const account=store[provider]?.accounts.find(x=>x.id===accountId);if(!account?.credential||account.paused||credentialGeneration(account.credential)!==generation)return false;if(writerGeneration<lastReconciledGeneration&&!liveOAuthAccountKeys.has(key))return false;if(isCurrent&&!isCurrent(store))return false;account.needsReauth=true;if(reason==="verify_account")account.needsReauthReason=reason;return true;},[provider,accountId,generation]);}
|
|
1512
|
+
|
|
1513
|
+
/** Attention is projected only; refresh selection continues to use the stored terminal flag. */
|
|
1514
|
+
export function accountNeedsReauthForStatus(provider: string, account: ProviderAccount, now = Date.now()): boolean {
|
|
1515
|
+
return account.needsReauth === true || (provider === "kiro" && account.credential.expires <= now
|
|
1516
|
+
&& account.refreshAttentionGeneration === credentialGeneration(account.credential));
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
/** Record refresh evidence under the same generation/config/pause fences as terminal health. */
|
|
1520
|
+
export async function markAccountRefreshAttentionIfGeneration(
|
|
1521
|
+
provider: string, accountId: string, generation: string, writerGeneration = captureConfigGeneration(),
|
|
1522
|
+
): Promise<boolean> {
|
|
1523
|
+
if (provider !== "kiro") return false;
|
|
1524
|
+
const key = oauthAccountKey(provider, accountId);
|
|
1525
|
+
return await mutateStore(store => {
|
|
1526
|
+
const account = store[provider]?.accounts.find(a => a.id === accountId);
|
|
1527
|
+
if (!account || account.paused || credentialGeneration(account.credential) !== generation) return false;
|
|
1528
|
+
if (writerGeneration < lastReconciledGeneration && !liveOAuthAccountKeys.has(key)) return false;
|
|
1529
|
+
account.refreshAttentionGeneration = generation;
|
|
1530
|
+
return true;
|
|
1531
|
+
}, [provider, accountId, generation]);
|
|
1532
|
+
}
|
|
@@ -29,6 +29,7 @@ import { codexWarmupFailureReason, warmCodexAccount } from "../codex/warmup";
|
|
|
29
29
|
import { getMainAccountToken, MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
|
|
30
30
|
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
|
|
31
31
|
import { providerCodexAccountMode } from "../providers/registry";
|
|
32
|
+
import { configuredAnthropicInstance } from "../providers/anthropic-instance";
|
|
32
33
|
import { captureConfigGeneration, type GenerationContext } from "../lib/state-store-sweeper";
|
|
33
34
|
import { tryAcquireNativeMainProfileClaim } from "../codex/native-main-admission";
|
|
34
35
|
|
|
@@ -144,6 +145,7 @@ export async function guardianSweep(nowMs: number = Date.now()): Promise<Guardia
|
|
|
144
145
|
// skipping accounts already marked needsReauth (terminal; only a re-login fixes them) or
|
|
145
146
|
// paused by the operator (manual exclusion from all automatic account use).
|
|
146
147
|
for (const provider of listOAuthProviders()) {
|
|
148
|
+
if (provider === "anthropic2" && configuredAnthropicInstance(config, provider) === undefined) continue;
|
|
147
149
|
if (resolveRefreshPolicy(provider, config) !== "proactive") continue;
|
|
148
150
|
for (const account of listAccounts(provider)) {
|
|
149
151
|
if (account.needsReauth || account.paused) continue;
|
package/src/oauth/types.ts
CHANGED
|
@@ -90,6 +90,8 @@ export interface ProviderAccount {
|
|
|
90
90
|
credential: OAuthCredentials;
|
|
91
91
|
/** Terminal refresh failure (invalid_grant / reused / revoked) — re-login required. */
|
|
92
92
|
needsReauth?: boolean;
|
|
93
|
+
/** Kiro SSO refresh input-error evidence; status-only attention after this generation expires. */
|
|
94
|
+
refreshAttentionGeneration?: string;
|
|
93
95
|
/** Operator exclusion from generic OAuth account selection until explicitly resumed. */
|
|
94
96
|
paused?: boolean;
|
|
95
97
|
/** Anthropic-only usage-switch override; absent inherits its pool default, zero disables it. */
|
|
@@ -23,6 +23,7 @@ import { getRoutingProfile, POLICY_NAMESPACE, resolvePolicyProfileId } from "../
|
|
|
23
23
|
import { resolveWireProtocolOverride } from "../server/adapter-resolve";
|
|
24
24
|
import { nativeChatDeclineReason } from "../server/chat-native-eligibility";
|
|
25
25
|
import { nativeMessagesDeclineReason } from "../server/messages-native-eligibility";
|
|
26
|
+
import { messagesSelectorTargetsSecondaryInstance, messagesSecondaryInstanceUnavailable } from "../server/messages-native-selector";
|
|
26
27
|
import { parseSyntheticRowId } from "../server/fast-row";
|
|
27
28
|
import type { OcxConfig } from "../types";
|
|
28
29
|
import { inboundWireForProtocol, type Protocol, type ProtocolReasonCode } from "./contract";
|
|
@@ -161,6 +162,7 @@ function messagesPassthroughPossible(config: OcxConfig, model: string): boolean
|
|
|
161
162
|
if (config.claudeCode?.nativePassthrough === false) return false;
|
|
162
163
|
if (!/^(claude|anthropic)/i.test(model)) return false;
|
|
163
164
|
try {
|
|
165
|
+
if (messagesSelectorTargetsSecondaryInstance(config, model)) return false;
|
|
164
166
|
return resolveInboundModel(model, config.claudeCode) === model;
|
|
165
167
|
} catch {
|
|
166
168
|
return false;
|
|
@@ -204,6 +206,9 @@ export function buildProtocolPlanSnapshot(
|
|
|
204
206
|
// A synthetic row needs the proxy-owned adapter, so it never takes the passthrough.
|
|
205
207
|
if (!syntheticRow && messagesPassthroughPossible(config, request.model)) reasonCodes.push("caller-credential-required");
|
|
206
208
|
try {
|
|
209
|
+
if (messagesSecondaryInstanceUnavailable(config, routeKey)) {
|
|
210
|
+
return { ...base, routeKind: "unknown", candidates: [], reasonCodes: [...reasonCodes, "unknown-model"] };
|
|
211
|
+
}
|
|
207
212
|
routeKey = resolveInboundModel(routeKey, config.claudeCode);
|
|
208
213
|
} catch {
|
|
209
214
|
return { ...base, routeKind: "unknown", candidates: [], reasonCodes };
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Nothing else reads those keys directly: endpoint admission, `count_tokens`, the endpoint
|
|
5
5
|
* metadata DTO and the dashboard all resolve through here, so they cannot disagree about
|
|
6
|
-
* whether an API is open.
|
|
6
|
+
* whether an API is open. Instance admission reads only config and registry metadata;
|
|
7
|
+
* this module never reads credentials or selects an account.
|
|
7
8
|
*/
|
|
8
9
|
import type { OcxConfig } from "../types";
|
|
10
|
+
import { configuredAnthropicInstance } from "../providers/anthropic-instance";
|
|
9
11
|
import type { Protocol } from "./contract";
|
|
10
12
|
|
|
11
13
|
export type ApiSurfaceSource =
|
|
@@ -70,8 +72,10 @@ export interface ProtocolSettings {
|
|
|
70
72
|
rollout: Readonly<ProtocolRolloutSettings>;
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
type ProtocolConfig = Pick<OcxConfig, "protocols" | "anthropicAccountPool"> & Partial<Pick<OcxConfig, "providers">>;
|
|
76
|
+
|
|
73
77
|
/** Resolve protocol policy with conservative defaults for every absent or malformed field. */
|
|
74
|
-
export function resolveProtocolSettings(config:
|
|
78
|
+
export function resolveProtocolSettings(config: ProtocolConfig, providerName?: string): Readonly<ProtocolSettings> {
|
|
75
79
|
const raw: unknown = config.protocols;
|
|
76
80
|
const protocols = isRec(raw) ? raw : {};
|
|
77
81
|
const rollout = isRec(protocols.rollout) ? protocols.rollout : {};
|
|
@@ -79,9 +83,11 @@ export function resolveProtocolSettings(config: Pick<OcxConfig, "protocols" | "a
|
|
|
79
83
|
// A present malformed container cannot become an absent/default-on policy.
|
|
80
84
|
const validContainers = (raw === undefined || isRec(raw))
|
|
81
85
|
&& (!Object.hasOwn(protocols, "rollout") || isRec(protocols.rollout));
|
|
82
|
-
const
|
|
86
|
+
const instance = configuredAnthropicInstance({ providers: config.providers ?? {} }, providerName);
|
|
87
|
+
const rawPool: unknown = instance === "anthropic2"
|
|
88
|
+
? config.providers?.anthropic2?.anthropicAccountPool : instance === "anthropic" ? config.anthropicAccountPool : undefined;
|
|
83
89
|
const pool = isRec(rawPool) ? rawPool : {};
|
|
84
|
-
const pooled =
|
|
90
|
+
const pooled = instance !== undefined && pool.enabled === true;
|
|
85
91
|
const poolPreference = !Object.hasOwn(pool, "nativeMessages") || pool.nativeMessages === true;
|
|
86
92
|
const nativeOn = (key: "managedMessagesNative" | "managedMessagesNativeOAuth") => validContainers
|
|
87
93
|
&& (!pooled || poolPreference)
|
|
@@ -105,15 +111,17 @@ export function resolveProtocolSettings(config: Pick<OcxConfig, "protocols" | "a
|
|
|
105
111
|
* dashboard can tell a preview computed under an older policy from a current one. Not a
|
|
106
112
|
* security boundary; FNV-1a over a canonical JSON projection.
|
|
107
113
|
*/
|
|
108
|
-
export function protocolPolicyRevision(config: Pick<OcxConfig, "apiSurfaces" | "claudeCode"
|
|
114
|
+
export function protocolPolicyRevision(config: ProtocolConfig & Pick<OcxConfig, "apiSurfaces" | "claudeCode">): string {
|
|
109
115
|
const surfaces = resolveApiSurfaceSettings(config);
|
|
110
116
|
const settings = resolveProtocolSettings(config);
|
|
111
117
|
const anthropicSettings = resolveProtocolSettings(config, "anthropic");
|
|
118
|
+
const secondarySettings = resolveProtocolSettings(config, "anthropic2");
|
|
112
119
|
const state = (record: unknown, key: string) => !isRec(record) || !Object.hasOwn(record, key)
|
|
113
120
|
? "absent" : record[key] === true ? "true" : record[key] === false ? "false" : "invalid";
|
|
114
121
|
const container = (value: unknown) => value === undefined ? "absent" : isRec(value) ? "object" : "invalid";
|
|
115
122
|
const rawProtocols: unknown = config.protocols;
|
|
116
123
|
const rawRollout = isRec(rawProtocols) ? rawProtocols.rollout : undefined;
|
|
124
|
+
const secondary = config.providers?.anthropic2;
|
|
117
125
|
const canonical = JSON.stringify({
|
|
118
126
|
messages: [surfaces.messages.enabled, surfaces.messages.source],
|
|
119
127
|
unrepresentable: settings.unrepresentable,
|
|
@@ -122,6 +130,11 @@ export function protocolPolicyRevision(config: Pick<OcxConfig, "apiSurfaces" | "
|
|
|
122
130
|
state(rawRollout, "managedMessagesNative"), state(rawRollout, "managedMessagesNativeOAuth"),
|
|
123
131
|
container(config.anthropicAccountPool), state(config.anthropicAccountPool, "enabled"),
|
|
124
132
|
state(config.anthropicAccountPool, "nativeMessages"),
|
|
133
|
+
container(secondary), state(secondary, "disabled"),
|
|
134
|
+
!!secondary && Object.hasOwn(secondary, "anthropicOAuthInstance"), secondary?.anthropicOAuthInstance ?? "absent",
|
|
135
|
+
secondary?.adapter ?? "absent", secondary?.authMode ?? "absent", secondary?.baseUrl ?? "absent",
|
|
136
|
+
container(secondary?.anthropicAccountPool), state(secondary?.anthropicAccountPool, "enabled"),
|
|
137
|
+
state(secondary?.anthropicAccountPool, "nativeMessages"),
|
|
125
138
|
],
|
|
126
139
|
rollout: [
|
|
127
140
|
settings.rollout.nativeChatCombos,
|
|
@@ -129,6 +142,8 @@ export function protocolPolicyRevision(config: Pick<OcxConfig, "apiSurfaces" | "
|
|
|
129
142
|
settings.rollout.managedMessagesNativeOAuth,
|
|
130
143
|
anthropicSettings.rollout.managedMessagesNative,
|
|
131
144
|
anthropicSettings.rollout.managedMessagesNativeOAuth,
|
|
145
|
+
secondarySettings.rollout.managedMessagesNative,
|
|
146
|
+
secondarySettings.rollout.managedMessagesNativeOAuth,
|
|
132
147
|
settings.rollout.directEncoders,
|
|
133
148
|
settings.rollout.shadowPlan,
|
|
134
149
|
],
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity of the two builtin Anthropic OAuth account-pool instances.
|
|
3
|
+
*
|
|
4
|
+
* This module has no imports on purpose: the provider registry and config types depend on it, and
|
|
5
|
+
* `./anthropic-instance.ts` (which reads the registry) builds on top of it. Keep it that way, or the
|
|
6
|
+
* registry and its lookup helper import each other.
|
|
7
|
+
*
|
|
8
|
+
* `anthropic` is the original pool. `anthropic2` is a second, independent pool that runs the same
|
|
9
|
+
* Anthropic implementation with its own credentials, pool settings and runtime state. Exactly these two
|
|
10
|
+
* IDs exist; a prefix such as `anthropic3` is never an instance.
|
|
11
|
+
*/
|
|
12
|
+
export const ANTHROPIC_INSTANCE_IDS = ["anthropic", "anthropic2"] as const;
|
|
13
|
+
export type AnthropicInstanceId = typeof ANTHROPIC_INSTANCE_IDS[number];
|
|
14
|
+
|
|
15
|
+
/** The canonical pool. Bare `claude-*` inference, Claude Code caller-forward and CLI import stay here. */
|
|
16
|
+
export const ANTHROPIC_PRIMARY_INSTANCE: AnthropicInstanceId = "anthropic";
|
|
17
|
+
/** The second, independent pool ("Anthropic · Pool 2"). Browser OAuth only. */
|
|
18
|
+
export const ANTHROPIC_POOL2_INSTANCE: AnthropicInstanceId = "anthropic2";
|
|
19
|
+
|
|
20
|
+
export type AnthropicInstanceRow = {
|
|
21
|
+
adapter?: string;
|
|
22
|
+
authMode?: string;
|
|
23
|
+
baseUrl?: string;
|
|
24
|
+
anthropicOAuthInstance?: unknown;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function isAnthropicInstanceId(value: unknown): value is AnthropicInstanceId {
|
|
28
|
+
return value === "anthropic" || value === "anthropic2";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Whether a configured provider row may be treated as the builtin instance of that name.
|
|
33
|
+
*
|
|
34
|
+
* `anthropic` keeps its historical meaning: the row named `anthropic` is the instance. A row named
|
|
35
|
+
* `anthropic2` may predate the builtin as a user's custom provider, so it is the builtin only when it is
|
|
36
|
+
* explicitly marked as an Anthropic OAuth instance. Transport endpoints never establish ownership.
|
|
37
|
+
* Anything else keeps its custom meaning and is never pinned, enriched or routed as the builtin pool.
|
|
38
|
+
*/
|
|
39
|
+
export function anthropicInstanceRowShapeMatches(
|
|
40
|
+
name: string,
|
|
41
|
+
row: AnthropicInstanceRow | undefined,
|
|
42
|
+
): boolean {
|
|
43
|
+
if (name === "anthropic") return true;
|
|
44
|
+
if (name !== "anthropic2" || !row) return false;
|
|
45
|
+
return Object.hasOwn(row, "anthropicOAuthInstance") && row.anthropicOAuthInstance === "anthropic2"
|
|
46
|
+
&& row.adapter === "anthropic" && row.authMode === "oauth";
|
|
47
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry- and config-aware Anthropic instance checks. See `./anthropic-instance-id.ts` for the IDs and
|
|
3
|
+
* the pure row-shape rule; this module adds the registry declaration and the configured-row lookup.
|
|
4
|
+
*
|
|
5
|
+
* Instance membership is never inferred from `adapter === "anthropic"` (the API-key provider and
|
|
6
|
+
* Anthropic-compatible gateways share that adapter) or from a name prefix.
|
|
7
|
+
*/
|
|
8
|
+
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
9
|
+
import { getProviderRegistryEntry } from "./registry";
|
|
10
|
+
import { anthropicInstanceRowShapeMatches, isAnthropicInstanceId, type AnthropicInstanceId } from "./anthropic-instance-id";
|
|
11
|
+
|
|
12
|
+
export * from "./anthropic-instance-id";
|
|
13
|
+
|
|
14
|
+
/** Exact registry check: one of the two IDs, declared as an Anthropic-family OAuth entry with its own OAuth ID. */
|
|
15
|
+
export function isAnthropicOAuthInstance(id: string | undefined): id is AnthropicInstanceId {
|
|
16
|
+
if (!isAnthropicInstanceId(id)) return false;
|
|
17
|
+
const entry = getProviderRegistryEntry(id);
|
|
18
|
+
return entry !== undefined
|
|
19
|
+
&& entry.authKind === "oauth"
|
|
20
|
+
&& entry.oauthId === id
|
|
21
|
+
&& entry.oauthFamily === "anthropic";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isBuiltinAnthropicInstanceRow(
|
|
25
|
+
name: string,
|
|
26
|
+
row?: Partial<Pick<OcxProviderConfig, "adapter" | "authMode" | "baseUrl" | "anthropicOAuthInstance">>,
|
|
27
|
+
): boolean {
|
|
28
|
+
return isAnthropicOAuthInstance(name) && anthropicInstanceRowShapeMatches(name, row);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The instance a configured provider name runs as, or undefined.
|
|
33
|
+
*
|
|
34
|
+
* For `anthropic` this is a compatibility identity result: it returns the instance whenever the name is
|
|
35
|
+
* `anthropic`, and callers keep every auth-mode, enabled-provider and eligibility check they already
|
|
36
|
+
* apply. For `anthropic2` the row must exist, must not be disabled and must pass the builtin row-shape
|
|
37
|
+
* rule, so an orphan `anthropic2` auth row or a custom provider of that name never activates the pool.
|
|
38
|
+
*/
|
|
39
|
+
export function configuredAnthropicInstance(
|
|
40
|
+
config: Pick<OcxConfig, "providers">,
|
|
41
|
+
name: string | undefined,
|
|
42
|
+
): AnthropicInstanceId | undefined {
|
|
43
|
+
if (!isAnthropicOAuthInstance(name)) return undefined;
|
|
44
|
+
if (name === "anthropic") return "anthropic";
|
|
45
|
+
const row = config.providers?.[name];
|
|
46
|
+
if (!row || row.disabled === true) return undefined;
|
|
47
|
+
return anthropicInstanceRowShapeMatches(name, row) ? name : undefined;
|
|
48
|
+
}
|