@bitkyc08/opencodex 2.86.0 → 2.87.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/gui/dist/assets/App-ZfYbLHkB.js +58 -0
- package/gui/dist/assets/App-wT3mXqFz.css +1 -0
- package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-DCd-A1az.js} +1 -1
- package/gui/dist/assets/index-CfYDMj34.js +94 -0
- package/gui/dist/assets/index-DPeB9HpV.css +1 -0
- package/gui/dist/assets/quota-summary-ChvmKGOk.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +9 -8
- package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
- package/src/adapters/anthropic/passthrough.ts +7 -23
- package/src/adapters/anthropic-progress.ts +84 -0
- package/src/adapters/anthropic.ts +108 -21
- package/src/adapters/base.ts +2 -0
- package/src/adapters/claude-cli/adapter.ts +111 -23
- package/src/adapters/claude-cli/profiles.ts +2 -1
- package/src/adapters/claude-cli/stable-replay.ts +108 -0
- package/src/adapters/claude-cli/usage-admission.ts +230 -0
- package/src/adapters/coding-agent/protocol.ts +1 -1
- package/src/adapters/coding-agent/turn.ts +18 -2
- package/src/adapters/dsh-account.ts +97 -0
- package/src/adapters/google.ts +35 -8
- package/src/adapters/openai-chat/messages.ts +4 -3
- package/src/adapters/openai-chat/passthrough.ts +8 -2
- package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
- package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
- package/src/adapters/openai-responses/passthrough.ts +57 -125
- package/src/adapters/openai-responses/request-strips.ts +25 -3
- package/src/adapters/openai-responses/transport.ts +81 -0
- package/src/adapters/qoder/adapter.ts +4 -2
- package/src/adapters/registry.ts +5 -0
- package/src/adapters/zed.ts +26 -1
- package/src/advisor/activity.ts +55 -0
- package/src/advisor/budget.ts +62 -0
- package/src/advisor/consult.ts +4 -1
- package/src/advisor/context.ts +16 -4
- package/src/advisor/evidence-tool.ts +21 -0
- package/src/advisor/evidence.ts +117 -0
- package/src/advisor/runtime.ts +79 -31
- package/src/advisor/settings.ts +11 -2
- package/src/bridge/errors.ts +19 -1
- package/src/bridge/internal.ts +3 -1
- package/src/bridge/response-json.ts +18 -3
- package/src/bridge/sse.ts +58 -8
- package/src/bridge/terminal-refusal.ts +59 -0
- package/src/claude/desktop-3p-library.ts +32 -7
- package/src/claude/desktop-3p.ts +55 -13
- package/src/claude/desktop-first-party.ts +1 -1
- package/src/claude/desktop-picker-platform.ts +4 -0
- package/src/claude/desktop-picker.ts +15 -8
- package/src/claude/desktop-system-proxy.ts +50 -4
- package/src/claude/inbound-content-options.ts +11 -0
- package/src/claude/inbound.ts +33 -0
- package/src/claude/intercept/picker-ca-startup.ts +2 -1
- package/src/claude/intercept/picker-runtime.ts +6 -5
- package/src/claude/intercept/picker-trust-windows.ts +338 -0
- package/src/claude/intercept/picker-trust.ts +19 -0
- package/src/claude/intercept/port-policy.ts +30 -0
- package/src/claude/intercept/runtime.ts +97 -94
- package/src/claude/outbound.ts +38 -13
- package/src/cli/account-api.ts +1 -0
- package/src/cli/account-policy-dto.ts +6 -0
- package/src/cli/account-policy.ts +34 -5
- package/src/cli/account-preservation.ts +44 -0
- package/src/cli/account.ts +8 -2
- package/src/cli/advisor.ts +17 -3
- package/src/cli/capabilities-accounts.ts +20 -1
- package/src/cli/capabilities-agents-routing.ts +5 -2
- package/src/cli/capabilities-base.ts +38 -10
- package/src/cli/capabilities-observe-system.ts +63 -3
- package/src/cli/capabilities-provider-models.ts +11 -0
- package/src/cli/codex-cli-update.ts +151 -41
- package/src/cli/codex-provider.ts +44 -0
- package/src/cli/combo-input.ts +10 -1
- package/src/cli/combo-result.ts +1 -1
- package/src/cli/combo.ts +2 -1
- package/src/cli/cross-home-owner.ts +17 -14
- package/src/cli/dispatch.ts +25 -4
- package/src/cli/doctor.ts +6 -1
- package/src/cli/ensure-desktop-cold-start.ts +60 -7
- package/src/cli/help.ts +3 -2
- package/src/cli/index.ts +1 -1
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/log-history.ts +172 -0
- package/src/cli/models-runtime-subcommands.ts +1 -0
- package/src/cli/models-runtime.ts +72 -8
- package/src/cli/observe-stream.ts +2 -0
- package/src/cli/observe.ts +16 -4
- package/src/cli/provider.ts +27 -5
- package/src/cli/registry.ts +28 -8
- package/src/cli/runtime-api.ts +5 -3
- package/src/cli/system-command.ts +40 -8
- package/src/cli/uninstall-client-state.ts +32 -0
- package/src/cli/usage-compare.ts +309 -0
- package/src/cli/usage-model-search.ts +4 -4
- package/src/cli/usage-report.ts +2 -2
- package/src/cli/v2.ts +1 -1
- package/src/client/machine-listener.ts +1 -3
- package/src/client/runtime.ts +1 -0
- package/src/clients/client-path.ts +51 -0
- package/src/clients/config-export/contracts.ts +2 -1
- package/src/clients/config-export.ts +18 -44
- package/src/clients/gentle-shell-path.ts +87 -0
- package/src/codex/account-auto-switch.ts +2 -0
- package/src/codex/account-identity.ts +29 -0
- package/src/codex/account-label.ts +1 -1
- package/src/codex/account-lifecycle.ts +21 -5
- package/src/codex/account-preservation.ts +28 -0
- package/src/codex/account-use-remaining.ts +23 -0
- package/src/codex/app-server-processes.ts +97 -7
- package/src/codex/app-server-restart-service.ts +55 -4
- package/src/codex/auth-api/account-list.ts +4 -0
- package/src/codex/auth-api/account-pause-group.ts +6 -50
- package/src/codex/auth-api/account-preservation.ts +57 -0
- package/src/codex/auth-api/routes.ts +8 -0
- package/src/codex/auth-api/use-remaining-route.ts +36 -0
- package/src/codex/auth-api.ts +0 -1
- package/src/codex/auth-context.ts +2 -2
- package/src/codex/catalog/auto-review.ts +20 -3
- package/src/codex/catalog/build-entries.ts +9 -1
- package/src/codex/catalog/combo-member.ts +14 -5
- package/src/codex/catalog/configured-tiers.ts +1 -1
- package/src/codex/catalog/gather-capture.ts +2 -0
- package/src/codex/catalog/metadata.ts +11 -9
- package/src/codex/catalog/model-hints.ts +21 -16
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/parsing.ts +30 -86
- package/src/codex/catalog/provider-models.ts +36 -34
- package/src/codex/catalog/routed-gather.ts +12 -13
- package/src/codex/cli-install-provenance.ts +105 -17
- package/src/codex/cli-update-plan.ts +545 -0
- package/src/codex/config-write-lock.ts +23 -2
- package/src/codex/desktop-app/picker-freshness.ts +96 -0
- package/src/codex/desktop-app-restart.ts +21 -0
- package/src/codex/desktop-compatibility/installed-build.ts +24 -6
- package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
- package/src/codex/desktop-compatibility/runtime.ts +12 -3
- package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
- package/src/codex/features.ts +85 -45
- package/src/codex/inject/history-failure.ts +27 -0
- package/src/codex/inject/restore.ts +24 -2
- package/src/codex/inject/routing-classify.ts +4 -3
- package/src/codex/inject.ts +18 -27
- package/src/codex/main-account-hard-lock.ts +30 -1
- package/src/codex/native-profile-startup.ts +1 -1
- package/src/codex/ocx-compaction-history.ts +12 -10
- package/src/codex/paths.ts +86 -1
- package/src/codex/provider-routing.ts +174 -0
- package/src/codex/refresh.ts +2 -2
- package/src/codex/routing/health-store.ts +1 -1
- package/src/codex/routing/selection.ts +4 -3
- package/src/codex/routing/thread-affinity.ts +1 -1
- package/src/codex/signed-in-identity.ts +43 -0
- package/src/codex/subagent-model-fallback.ts +1 -1
- package/src/codex/sync.ts +6 -1
- package/src/codex/user-identity.ts +16 -5
- package/src/codex/windows-local-appdata.ts +84 -0
- package/src/combos/failover.ts +6 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +46 -10
- package/src/combos/types.ts +15 -0
- package/src/config/diagnostics.ts +33 -6
- package/src/config/live-reconcile.ts +38 -2
- package/src/config/load-degrade.ts +17 -0
- package/src/config/opencode-wire-upgrade.ts +54 -0
- package/src/config/opencode-zen-upgrade.ts +52 -0
- package/src/config/owner-registry.ts +22 -17
- package/src/config/provider-validation.ts +45 -2
- package/src/config/schema/config-schema.ts +22 -0
- package/src/config/schema/leaf-validators.ts +38 -9
- package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
- package/src/config/schema/native-reasoning-retention.ts +26 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +608 -284
- package/src/grok/sync.ts +1 -1
- package/src/images/loop.ts +46 -8
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +1 -0
- package/src/integrations/registry.ts +8 -0
- package/src/lib/codex-restart-contract.ts +15 -0
- package/src/lib/desktop-upstream-tunnel.ts +18 -53
- package/src/lib/empty-tool-catalog.ts +62 -0
- package/src/lib/errors.ts +19 -3
- package/src/lib/gcp-adc.ts +96 -9
- package/src/lib/gcp-credential-input.ts +12 -0
- package/src/lib/lab-live-route-production.ts +5 -0
- package/src/lib/opaque-proxy-tunnel.ts +217 -0
- package/src/lib/redact.ts +2 -2
- package/src/oauth/account-quota-rank.ts +69 -2
- package/src/oauth/anthropic-account-refusal.ts +20 -2
- package/src/oauth/anthropic-oauth-definitions.ts +3 -1
- package/src/oauth/anthropic-routing.ts +1 -21
- package/src/oauth/anthropic.ts +38 -7
- package/src/oauth/callback-server.ts +27 -1
- package/src/oauth/chatgpt-device.ts +1 -1
- package/src/oauth/chatgpt-shared.ts +83 -0
- package/src/oauth/chatgpt.ts +8 -72
- package/src/oauth/dsh.ts +203 -0
- package/src/oauth/generic-account-failover.ts +33 -7
- package/src/oauth/generic-session-affinity.ts +125 -0
- package/src/oauth/index.ts +184 -109
- package/src/oauth/local-token-detect.ts +21 -1
- package/src/oauth/login-cli-errors.ts +44 -0
- package/src/oauth/login-cli.ts +69 -7
- package/src/oauth/login-flow-state.ts +371 -23
- package/src/oauth/meta-muse-errors.ts +41 -0
- package/src/oauth/meta-muse-keychain.ts +52 -0
- package/src/oauth/meta-muse.ts +51 -84
- package/src/oauth/model-discovery-auth.ts +3 -0
- package/src/oauth/pool-settings-capability.ts +4 -2
- package/src/oauth/refresh-flight-retirement.ts +48 -0
- package/src/oauth/session-affinity-key.ts +23 -0
- package/src/oauth/store-anthropic-instance.ts +9 -2
- package/src/oauth/store.ts +12 -5
- package/src/protocols/encoders/adapter-events.ts +5 -4
- package/src/protocols/encoders/messages.ts +9 -6
- package/src/protocols/provider-summary.ts +2 -1
- package/src/providers/api-key-cooldowns.ts +49 -0
- package/src/providers/api-key-resolve.ts +11 -6
- package/src/providers/api-key-response-ownership.ts +57 -0
- package/src/providers/api-key-selection.ts +31 -6
- package/src/providers/api-keys.ts +214 -3
- package/src/providers/claude-cli-usage.ts +96 -0
- package/src/providers/dsh-account-destination.ts +31 -0
- package/src/providers/dsh-account-models.ts +41 -0
- package/src/providers/gcp-credential-cleanup.ts +59 -0
- package/src/providers/key-failover.ts +54 -50
- package/src/providers/key-store.ts +7 -2
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/model-rename-migration.ts +2 -2
- package/src/providers/openai-tiers-destination.ts +11 -2
- package/src/providers/openai-tiers.ts +5 -12
- package/src/providers/opencode-go-transport.ts +3 -1
- package/src/providers/quota/account-cache.ts +6 -5
- package/src/providers/quota/account-row.ts +34 -0
- package/src/providers/quota/antigravity.ts +28 -7
- package/src/providers/quota/dsh-account.ts +70 -0
- package/src/providers/quota/vendor-probes-oauth.ts +1 -93
- package/src/providers/quota-types.ts +2 -0
- package/src/providers/quota.ts +14 -20
- package/src/providers/registry/entries-core.ts +37 -8
- package/src/providers/registry/types.ts +1 -1
- package/src/providers/request-pacing.ts +77 -19
- package/src/providers/routed-config.ts +339 -0
- package/src/providers/service-tier.ts +2 -1
- package/src/quota/window-mapping.ts +5 -2
- package/src/responses/anthropic-progress.ts +34 -0
- package/src/responses/assistant-replay.ts +35 -0
- package/src/responses/compaction-history-framing.ts +65 -0
- package/src/responses/compaction.ts +6 -1
- package/src/responses/incomplete-reason.ts +31 -0
- package/src/responses/parser.ts +50 -5
- package/src/responses/reasoning-envelope.ts +4 -0
- package/src/responses/reasoning-replay-cache.ts +24 -2
- package/src/responses/reasoning-retention.ts +173 -0
- package/src/responses/retained-compaction.ts +46 -0
- package/src/responses/schema.ts +23 -0
- package/src/router.ts +8 -321
- package/src/routing/health.ts +37 -22
- package/src/routing/history/indexer.ts +6 -5
- package/src/routing/history/ingest.ts +6 -2
- package/src/routing/profile.ts +1 -1
- package/src/server/admission-model-scope.ts +17 -0
- package/src/server/auth-cors.ts +11 -0
- package/src/server/chat-completions.ts +7 -1
- package/src/server/chat-native-reasoning.ts +123 -0
- package/src/server/chat-native-sse.ts +110 -7
- package/src/server/chat-native.ts +76 -17
- package/src/server/claude-messages.ts +55 -414
- package/src/server/effort-policy.ts +1 -1
- package/src/server/images.ts +15 -2
- package/src/server/index/model-alias-id.ts +36 -0
- package/src/server/index/optional-listeners.ts +2 -0
- package/src/server/index/serve-options.ts +9 -1
- package/src/server/index/startup-warnings.ts +2 -0
- package/src/server/index.ts +1 -1
- package/src/server/inference/advisor-diagnostics.ts +31 -0
- package/src/server/management/advisor-routes.ts +15 -4
- package/src/server/management/claude-desktop-picker-routes.ts +1 -1
- package/src/server/management/combo-routes.ts +176 -50
- package/src/server/management/companion-routes.ts +1 -1
- package/src/server/management/config-routes.ts +75 -4
- package/src/server/management/cursor-integration-routes.ts +1 -1
- package/src/server/management/model-routes.ts +11 -6
- package/src/server/management/model-rows.ts +14 -0
- package/src/server/management/native-integration-routes.ts +2 -7
- package/src/server/management/native-reasoning-retention-routes.ts +41 -0
- package/src/server/management/oauth-account-routes.ts +87 -14
- package/src/server/management/provider-gcp-credential-routes.ts +29 -0
- package/src/server/management/provider-key-pacing-routes.ts +31 -0
- package/src/server/management/provider-overwrite-carry.ts +7 -0
- package/src/server/management/provider-routes.ts +25 -30
- package/src/server/management/route-registry.ts +10 -1
- package/src/server/management-api.ts +3 -1
- package/src/server/messages-native-tool-guard.ts +185 -0
- package/src/server/messages-native.ts +101 -21
- package/src/server/messages-shared.ts +431 -0
- package/src/server/relay.ts +1 -1
- package/src/server/request-log-terminal-status.ts +1 -1
- package/src/server/request-log.ts +6 -1
- package/src/server/responses/account-change-state.ts +8 -2
- package/src/server/responses/adapter-continuation.ts +13 -1
- package/src/server/responses/adapter-delivery.ts +19 -6
- package/src/server/responses/adapter-dispatch.ts +19 -2
- package/src/server/responses/advisor-retention.ts +117 -0
- package/src/server/responses/advisor-slot.ts +73 -41
- package/src/server/responses/codex-ws-wire.ts +15 -8
- package/src/server/responses/compact.ts +303 -125
- package/src/server/responses/compaction-recovery.ts +18 -10
- package/src/server/responses/compaction-retention.ts +61 -0
- package/src/server/responses/context-overflow.ts +11 -6
- package/src/server/responses/core-auth.ts +6 -0
- package/src/server/responses/core-codex-account.ts +11 -1
- package/src/server/responses/core-combo-failure.ts +2 -0
- package/src/server/responses/core-combo.ts +120 -36
- package/src/server/responses/core-errors.ts +3 -3
- package/src/server/responses/core-lifetime.ts +5 -2
- package/src/server/responses/core-opaque-recovery.ts +35 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-pipeline.ts +141 -0
- package/src/server/responses/core-replay.ts +18 -2
- package/src/server/responses/core.ts +2 -115
- package/src/server/responses/fetch-helpers.ts +73 -19
- package/src/server/responses/main-hard-lock-routing.ts +109 -0
- package/src/server/responses/native-response-control.ts +4 -2
- package/src/server/responses/passthrough-delivery.ts +3 -1
- package/src/server/responses/passthrough-dispatch.ts +15 -13
- package/src/server/responses/request-prepare.ts +76 -12
- package/src/server/responses/request-sidecar-auth.ts +2 -2
- package/src/server/responses/request-transport.ts +104 -27
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +96 -30
- package/src/server/responses/sidecar-execution.ts +20 -2
- package/src/server/responses/terminal-guard.ts +2 -16
- package/src/service/auth-preflight.ts +71 -0
- package/src/service/cli.ts +4 -2
- package/src/service/definition.ts +97 -0
- package/src/service/guards.ts +1 -61
- package/src/service/health-sentinel-linux.ts +112 -0
- package/src/service/health-sentinel-main.ts +76 -57
- package/src/service/health.ts +5 -89
- package/src/service/launchd.ts +27 -7
- package/src/service/logging.ts +72 -9
- package/src/service/repair.ts +2 -1
- package/src/service/state.ts +7 -8
- package/src/service/systemd.ts +1 -1
- package/src/service/windows-guarded-manager.ts +2 -1
- package/src/service/windows-ops.ts +1 -1
- package/src/service/windows-scheduler.ts +2 -7
- package/src/service/windows-taskxml.ts +2 -2
- package/src/service/windows-wscript.ts +14 -0
- package/src/service.ts +4 -2
- package/src/sidecar/auth.ts +2 -0
- package/src/types/config.ts +64 -3
- package/src/types/opencode-go.ts +61 -0
- package/src/types/opencode-zen.ts +29 -0
- package/src/types/provider.ts +32 -6
- package/src/types/request.ts +23 -2
- package/src/types/wire.ts +29 -37
- package/src/types.ts +1 -0
- package/src/update/npm-invocation.mjs +12 -2
- package/src/usage/cost.ts +48 -12
- package/src/usage/expected-prices.ts +3 -0
- package/src/usage/ledger-retention.ts +2 -1
- package/src/usage/ledger-scanner.ts +6 -5
- package/src/usage/log.ts +17 -8
- package/src/usage/summary.ts +22 -9
- package/src/usage/user-cost-overlays.ts +14 -19
- package/src/usage/user-cost-validation.ts +38 -0
- package/src/vision/eligibility.ts +13 -0
- package/src/web-search/loop.ts +55 -5
- package/src/web-search/progress-stream.ts +4 -1
- package/src/web-search/run-turn-loop.ts +9 -1
- package/gui/dist/assets/App-BjlttVLy.js +0 -58
- package/gui/dist/assets/App-CSJvmjNs.css +0 -1
- package/gui/dist/assets/index-BB6uK6tU.css +0 -1
- package/gui/dist/assets/index-BMssP4k3.js +0 -94
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { MuseLoginError } from "./meta-muse-errors";
|
|
2
|
+
|
|
3
|
+
// Approval includes typing a password. Five seconds kills security while the user types.
|
|
4
|
+
export const MUSE_KEYCHAIN_TIMEOUT_MS = 120_000;
|
|
5
|
+
export interface MuseKeychainProcess {
|
|
6
|
+
stdout: ReadableStream<Uint8Array>;
|
|
7
|
+
exited: Promise<number>;
|
|
8
|
+
kill(): void;
|
|
9
|
+
}
|
|
10
|
+
interface KeychainDeps {
|
|
11
|
+
spawn?: (argv: string[]) => MuseKeychainProcess;
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function readMuseKeychain(signal?: AbortSignal, deps: KeychainDeps = {}): Promise<string> {
|
|
16
|
+
if (signal?.aborted) throw new MuseLoginError("cancelled");
|
|
17
|
+
const timeout = AbortSignal.timeout(deps.timeoutMs ?? MUSE_KEYCHAIN_TIMEOUT_MS);
|
|
18
|
+
const deadline = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
19
|
+
let child: MuseKeychainProcess | undefined;
|
|
20
|
+
let exited = false;
|
|
21
|
+
let onAbort: (() => void) | undefined;
|
|
22
|
+
try {
|
|
23
|
+
child = (deps.spawn ?? (argv => Bun.spawn(argv, { stdin: "ignore", stdout: "pipe", stderr: "ignore" })))(
|
|
24
|
+
["/usr/bin/security", "find-generic-password", "-s", "ai.meta.dev.credentials", "-a", "meta", "-w"],
|
|
25
|
+
);
|
|
26
|
+
const finished = Promise.all([new Response(child.stdout).text(), child.exited.then(code => { exited = true; return code; })]);
|
|
27
|
+
const aborted = new Promise<never>((_, reject) => {
|
|
28
|
+
onAbort = () => reject(new MuseLoginError(signal?.aborted ? "cancelled" : "keychain-timeout"));
|
|
29
|
+
if (deadline.aborted) onAbort();
|
|
30
|
+
else deadline.addEventListener("abort", onAbort, { once: true });
|
|
31
|
+
});
|
|
32
|
+
const [out, code] = await Promise.race([finished, aborted]);
|
|
33
|
+
if (signal?.aborted) throw new MuseLoginError("cancelled");
|
|
34
|
+
if (code !== 0) {
|
|
35
|
+
// security returns OSStatus truncated to an 8-bit process exit code (SecBase.h).
|
|
36
|
+
// Unknown failures stay unknown; no stderr or credential output is retained.
|
|
37
|
+
if (code === 44) throw new MuseLoginError("keychain-missing"); // errSecItemNotFound -25300
|
|
38
|
+
if (code === 36) throw new MuseLoginError("keychain-locked"); // errSecInteractionNotAllowed -25308
|
|
39
|
+
if (code === 51 || code === 128) throw new MuseLoginError("keychain-denied"); // -25293 / -128
|
|
40
|
+
throw new MuseLoginError("keychain-unreadable");
|
|
41
|
+
}
|
|
42
|
+
if (!out.trim()) throw new MuseLoginError("keychain-unreadable");
|
|
43
|
+
return out.trim();
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (signal?.aborted) throw new MuseLoginError("cancelled");
|
|
46
|
+
if (error instanceof MuseLoginError) throw error;
|
|
47
|
+
throw new MuseLoginError("keychain-unreadable");
|
|
48
|
+
} finally {
|
|
49
|
+
if (onAbort) deadline.removeEventListener("abort", onAbort);
|
|
50
|
+
if (child && !exited) { try { child.kill(); } catch { /* already exited */ } }
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/oauth/meta-muse.ts
CHANGED
|
@@ -26,14 +26,13 @@ import { homedir } from "node:os";
|
|
|
26
26
|
import { join } from "node:path";
|
|
27
27
|
import { sanitizeApiKeyValue } from "../providers/api-keys";
|
|
28
28
|
import { MuseDeviceLoginError, loginMetaMuseDevice } from "./meta-muse-device";
|
|
29
|
+
import { MuseLoginError, publicMuseLoginErrorMessage } from "./meta-muse-errors";
|
|
30
|
+
import { readMuseKeychain } from "./meta-muse-keychain";
|
|
29
31
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
30
32
|
|
|
31
33
|
const MUSE_POINTER_PATH = join(homedir(), ".config", "muse", "auth.json");
|
|
32
|
-
const KEYCHAIN_SERVICE = "ai.meta.dev.credentials";
|
|
33
|
-
const KEYCHAIN_ACCOUNT = "meta";
|
|
34
34
|
const MODELS_URL = "https://api.meta.ai/v1/models";
|
|
35
35
|
const VALIDATE_TIMEOUT_MS = 10_000;
|
|
36
|
-
const KEYCHAIN_TIMEOUT_MS = 5_000;
|
|
37
36
|
|
|
38
37
|
/**
|
|
39
38
|
* Shown BEFORE any credential is read.
|
|
@@ -82,44 +81,9 @@ export interface MuseLoginOptions {
|
|
|
82
81
|
async function defaultReadPointer(): Promise<string | null> {
|
|
83
82
|
try {
|
|
84
83
|
return await Bun.file(MUSE_POINTER_PATH).text();
|
|
85
|
-
} catch {
|
|
86
|
-
return null;
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* `security` can block indefinitely — the Keychain may raise an interactive approval
|
|
92
|
-
* prompt, and on a headless or locked machine nobody answers it. Without a deadline the
|
|
93
|
-
* login would hang before the validation timeout below is even created, so the bound
|
|
94
|
-
* lives here rather than only around the fetch.
|
|
95
|
-
*/
|
|
96
|
-
async function defaultReadKeychain(signal?: AbortSignal): Promise<string | null> {
|
|
97
|
-
const deadline = signal
|
|
98
|
-
? AbortSignal.any([signal, AbortSignal.timeout(KEYCHAIN_TIMEOUT_MS)])
|
|
99
|
-
: AbortSignal.timeout(KEYCHAIN_TIMEOUT_MS);
|
|
100
|
-
let proc: Bun.Subprocess<"ignore", "pipe", "pipe"> | undefined;
|
|
101
|
-
try {
|
|
102
|
-
proc = Bun.spawn(
|
|
103
|
-
["security", "find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", KEYCHAIN_ACCOUNT, "-w"],
|
|
104
|
-
{ stdout: "pipe", stderr: "pipe" },
|
|
105
|
-
);
|
|
106
|
-
const child = proc;
|
|
107
|
-
const finished = Promise.all([new Response(child.stdout).text(), child.exited]);
|
|
108
|
-
const timedOut = new Promise<null>((resolve) => {
|
|
109
|
-
if (deadline.aborted) { resolve(null); return; }
|
|
110
|
-
deadline.addEventListener("abort", () => resolve(null), { once: true });
|
|
111
|
-
});
|
|
112
|
-
const settled = await Promise.race([finished, timedOut]);
|
|
113
|
-
if (settled === null) return null;
|
|
114
|
-
const [out, code] = settled;
|
|
115
|
-
if (code !== 0) return null;
|
|
116
|
-
const trimmed = out.trim();
|
|
117
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
118
|
-
} catch {
|
|
119
|
-
return null;
|
|
120
|
-
} finally {
|
|
121
|
-
// A prompt still on screen keeps the child alive after the race resolves.
|
|
122
|
-
if (proc && proc.exitCode === null) { try { proc.kill(); } catch { /* already gone */ } }
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if ((error as { code?: string })?.code === "ENOENT") return null;
|
|
86
|
+
throw new MuseLoginError("pointer-unreadable");
|
|
123
87
|
}
|
|
124
88
|
}
|
|
125
89
|
|
|
@@ -160,6 +124,10 @@ async function manualKeyCredential(
|
|
|
160
124
|
return pasted.length > 0 ? pasted : null;
|
|
161
125
|
}
|
|
162
126
|
|
|
127
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
128
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
129
|
+
}
|
|
130
|
+
|
|
163
131
|
function normalizedEmail(value: unknown): string | undefined {
|
|
164
132
|
if (typeof value !== "string") return undefined;
|
|
165
133
|
const trimmed = value.trim().toLowerCase();
|
|
@@ -179,6 +147,7 @@ export async function loginMetaMuse(
|
|
|
179
147
|
// Before ANY read: the CLI has no other warning surface.
|
|
180
148
|
ctrl.onProgress?.(CONSENT_WARNING);
|
|
181
149
|
|
|
150
|
+
if (ctrl.signal?.aborted) throw new MuseLoginError("cancelled");
|
|
182
151
|
const platform = deps.platform ?? process.platform;
|
|
183
152
|
|
|
184
153
|
// Import first on darwin, and only for a plain login. A user who already ran `muse
|
|
@@ -187,8 +156,19 @@ export async function loginMetaMuse(
|
|
|
187
156
|
// a request to arrive at the same key. `forceLogin` maps to "off" upstream because
|
|
188
157
|
// reimporting is how an add-account silently re-adds the account the user already has.
|
|
189
158
|
if (platform === "darwin" && options.importLocal !== "off") {
|
|
190
|
-
|
|
191
|
-
|
|
159
|
+
try {
|
|
160
|
+
const imported = await importFromKeychain(ctrl, deps);
|
|
161
|
+
if (imported) return imported;
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (isCancellation(error, ctrl)) throw error;
|
|
164
|
+
const reason = publicMuseLoginErrorMessage(error);
|
|
165
|
+
// Recovery requires an explicit paste, never an automatic grant or key mint.
|
|
166
|
+
if (!reason || !ctrl.onManualCodeInput) throw error;
|
|
167
|
+
const pasted = await manualKeyCredential(ctrl, reason);
|
|
168
|
+
if (ctrl.signal?.aborted) throw new MuseLoginError("cancelled");
|
|
169
|
+
if (pasted === null) throw error;
|
|
170
|
+
return await validatedMetaMuseCredential(pasted, ctrl, deps, undefined, "manual");
|
|
171
|
+
}
|
|
192
172
|
}
|
|
193
173
|
|
|
194
174
|
// fetchImpl/sleep/now are FORWARDED. Without this the device module would carry its own
|
|
@@ -221,46 +201,46 @@ export async function loginMetaMuse(
|
|
|
221
201
|
* caller can offer a device grant instead of a dead end. Everything else still throws: a
|
|
222
202
|
* corrupt file, an unmeasured storage backend, a Keychain read that times out, and a
|
|
223
203
|
* Keychain entry that is unreadable or carries no usable key are all cases where a
|
|
224
|
-
* credential probably EXISTS
|
|
225
|
-
* login to work around a permissions dialog.
|
|
204
|
+
* credential probably EXISTS. The caller can offer explicit key paste recovery, but never
|
|
205
|
+
* silently starts a second login to work around a permissions dialog.
|
|
226
206
|
*/
|
|
227
207
|
async function importFromKeychain(
|
|
228
208
|
ctrl: OAuthController,
|
|
229
209
|
deps: MuseImportDeps,
|
|
230
210
|
): Promise<OAuthCredentials | null> {
|
|
231
211
|
const pointerRaw = await (deps.readPointer ?? defaultReadPointer)();
|
|
212
|
+
if (ctrl.signal?.aborted) throw new MuseLoginError("cancelled");
|
|
232
213
|
if (pointerRaw === null) return null;
|
|
233
214
|
|
|
234
215
|
let pointer: MusePointer;
|
|
235
216
|
try {
|
|
236
217
|
pointer = JSON.parse(pointerRaw) as MusePointer;
|
|
218
|
+
if (!isRecord(pointer) || (pointer.providers !== undefined && !isRecord(pointer.providers))
|
|
219
|
+
|| (pointer.providers?.meta !== undefined && !isRecord(pointer.providers.meta))) throw new Error();
|
|
237
220
|
} catch {
|
|
238
|
-
throw new
|
|
221
|
+
throw new MuseLoginError("pointer-invalid");
|
|
239
222
|
}
|
|
240
223
|
|
|
241
224
|
const meta = pointer.providers?.meta;
|
|
242
225
|
if (!meta || meta.mechanism !== "oauth") return null;
|
|
243
226
|
// A different storage backend is a shape we have not measured; refuse rather than guess.
|
|
244
227
|
if (meta.storage !== "keychain") {
|
|
245
|
-
throw new
|
|
246
|
-
`Muse Code stored its credential with an unsupported backend (${String(meta.storage)}); only the macOS Keychain is verified.`,
|
|
247
|
-
);
|
|
228
|
+
throw new MuseLoginError("storage-unsupported");
|
|
248
229
|
}
|
|
249
230
|
|
|
250
|
-
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
);
|
|
257
|
-
}
|
|
231
|
+
if (ctrl.signal?.aborted) throw new MuseLoginError("cancelled");
|
|
232
|
+
// Publish a hint before the OS dialog so the dashboard can poll and cancel it.
|
|
233
|
+
ctrl.onAuth?.({ url: "", instructions: "Approve the Muse Code Keychain prompt on the proxy host. Waiting up to 120 seconds." });
|
|
234
|
+
const secretRaw = await (deps.readKeychain ?? readMuseKeychain)(ctrl.signal);
|
|
235
|
+
if (ctrl.signal?.aborted) throw new MuseLoginError("cancelled");
|
|
236
|
+
if (secretRaw === null) throw new MuseLoginError("keychain-unreadable");
|
|
258
237
|
|
|
259
238
|
let secret: MuseKeychainSecret;
|
|
260
239
|
try {
|
|
261
240
|
secret = JSON.parse(secretRaw) as MuseKeychainSecret;
|
|
241
|
+
if (!isRecord(secret)) throw new Error();
|
|
262
242
|
} catch {
|
|
263
|
-
throw new
|
|
243
|
+
throw new MuseLoginError("keychain-invalid");
|
|
264
244
|
}
|
|
265
245
|
|
|
266
246
|
// access_token is present but 401s against the Model API (003 §B) — never fall back to it.
|
|
@@ -287,7 +267,7 @@ function deviceFailureReason(error: unknown): string {
|
|
|
287
267
|
case "subscription-inactive":
|
|
288
268
|
return "This Meta account has no active Muse Code subscription.";
|
|
289
269
|
case "entitlement-required":
|
|
290
|
-
return
|
|
270
|
+
return "Complete the Muse Code subscription setup at https://dev.meta.ai, then retry.";
|
|
291
271
|
case "mint-rate-limited":
|
|
292
272
|
return "Meta rate-limited the Muse Code key request.";
|
|
293
273
|
case "device-denied":
|
|
@@ -314,7 +294,7 @@ function noPasteSurfaceError(platform: string, reason: string): Error {
|
|
|
314
294
|
const platformClause = platform === "darwin"
|
|
315
295
|
? ""
|
|
316
296
|
: platform === "win32"
|
|
317
|
-
? "
|
|
297
|
+
? " The Windows Muse Code credential store has not been verified, so there is no credential to import through this reader."
|
|
318
298
|
: " The Muse Code CLI runs here, but where it stores its credential has not been measured, "
|
|
319
299
|
+ "so importing one is refused rather than guessed.";
|
|
320
300
|
return new Error(
|
|
@@ -338,22 +318,10 @@ async function validatedMetaMuseCredential(
|
|
|
338
318
|
email: string | undefined,
|
|
339
319
|
source: "local-cli" | "manual",
|
|
340
320
|
): Promise<OAuthCredentials> {
|
|
341
|
-
|
|
342
|
-
? `Copy it again from ${MANUAL_KEY_URL}.`
|
|
343
|
-
: "Run `muse login` again.";
|
|
321
|
+
if (ctrl.signal?.aborted) throw new MuseLoginError("cancelled");
|
|
344
322
|
const apiKey = sanitizeApiKeyValue(candidate);
|
|
345
|
-
if (!apiKey)
|
|
346
|
-
|
|
347
|
-
// that quietly rewrites a user-facing error is a behavior change in disguise.
|
|
348
|
-
throw new Error(source === "manual"
|
|
349
|
-
? `The pasted Muse Code credential carries no usable API key. ${retry}`
|
|
350
|
-
: "The Muse Code Keychain entry carries no usable API key. Run `muse login` again.");
|
|
351
|
-
}
|
|
352
|
-
if (!/^LLM\|\d+\|[A-Za-z0-9_-]{10,}$/.test(apiKey)) {
|
|
353
|
-
throw new Error(source === "manual"
|
|
354
|
-
? `The pasted Muse Code credential is not in the expected Meta API key format. ${retry}`
|
|
355
|
-
: "The Muse Code credential is not in the expected Meta API key format. Run `muse login` again.");
|
|
356
|
-
}
|
|
323
|
+
if (!apiKey) throw new MuseLoginError(source === "manual" ? "manual-key-missing" : "key-missing");
|
|
324
|
+
if (!/^LLM\|\d+\|[A-Za-z0-9_-]{10,}$/.test(apiKey)) throw new MuseLoginError(source === "manual" ? "manual-key-format" : "key-format");
|
|
357
325
|
ctrl.onProgress?.(source === "manual"
|
|
358
326
|
? "Validating the pasted Meta credential..."
|
|
359
327
|
: "Validating the imported Meta credential...");
|
|
@@ -366,18 +334,17 @@ async function validatedMetaMuseCredential(
|
|
|
366
334
|
let response: Response;
|
|
367
335
|
try {
|
|
368
336
|
response = await fetchImpl(MODELS_URL, {
|
|
369
|
-
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
|
|
337
|
+
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json", "x-api-version": "1.0.0" },
|
|
338
|
+
redirect: "error",
|
|
370
339
|
signal,
|
|
371
340
|
});
|
|
372
|
-
} catch
|
|
373
|
-
if (ctrl.signal?.aborted) throw
|
|
374
|
-
throw new
|
|
375
|
-
}
|
|
376
|
-
if (!response.ok) {
|
|
377
|
-
throw new Error(
|
|
378
|
-
`The Muse Code credential was rejected by the Meta Model API (HTTP ${response.status}). ${retry}`,
|
|
379
|
-
);
|
|
341
|
+
} catch {
|
|
342
|
+
if (ctrl.signal?.aborted) throw new MuseLoginError("cancelled");
|
|
343
|
+
throw new MuseLoginError(signal.aborted ? "validation-timeout" : "validation-network");
|
|
380
344
|
}
|
|
345
|
+
if (ctrl.signal?.aborted) throw new MuseLoginError("cancelled");
|
|
346
|
+
void response.body?.cancel().catch(() => {});
|
|
347
|
+
if (!response.ok) throw new MuseLoginError("validation-http", response.status);
|
|
381
348
|
|
|
382
349
|
return {
|
|
383
350
|
access: apiKey,
|
|
@@ -5,6 +5,9 @@ import { resolveProviderTransport } from "../providers/xai-transport";
|
|
|
5
5
|
|
|
6
6
|
/** Discovery owns its configured row (a frozen capture for catalog gathers), not live config. */
|
|
7
7
|
export function mayResolveModelsOAuth(name: string, provider: OcxProviderConfig | undefined): boolean {
|
|
8
|
+
// The DSH account token is pinned to its own adapter and fixed DeepSeek hosts. Its catalog is
|
|
9
|
+
// static, so no discovery path may resolve it, whatever adapter or baseUrl the row was edited to.
|
|
10
|
+
if (name === "dsh-account") return false;
|
|
8
11
|
return name !== "anthropic2" || (provider !== undefined && provider.disabled !== true
|
|
9
12
|
&& isBuiltinAnthropicInstanceRow(name, provider));
|
|
10
13
|
}
|
|
@@ -22,7 +22,7 @@ import { readAnthropicModelRoutes } from "./anthropic-model-routes";
|
|
|
22
22
|
*/
|
|
23
23
|
export type PoolSettingsKind = "codex" | "anthropic" | "generic";
|
|
24
24
|
|
|
25
|
-
export const GENERIC_POOL_STRATEGIES = ["quota", "round-robin", "fill-first", "least-loaded"] as const;
|
|
25
|
+
export const GENERIC_POOL_STRATEGIES = ["quota", "round-robin", "fill-first", "reset-first", "least-loaded"] as const;
|
|
26
26
|
export type GenericPoolStrategy = typeof GENERIC_POOL_STRATEGIES[number];
|
|
27
27
|
|
|
28
28
|
export function poolSettingsCapability(name: string, provider: OcxProviderConfig | undefined): PoolSettingsKind | null {
|
|
@@ -33,7 +33,9 @@ export function poolSettingsCapability(name: string, provider: OcxProviderConfig
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export function parseGenericPoolStrategy(value: unknown, providerName?: string): GenericPoolStrategy | null {
|
|
36
|
-
//
|
|
36
|
+
// reset-first is shared with the Codex pool, so it is named here rather than added to the
|
|
37
|
+
// three-name shared parser that the Anthropic kind also uses. least-loaded is Kiro-specific.
|
|
38
|
+
if (value === "reset-first") return value;
|
|
37
39
|
if (value === "least-loaded") return providerName === "kiro" ? value : null;
|
|
38
40
|
return parseAccountPoolStrategy(value);
|
|
39
41
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createAdmissionGate } from "../lib/admission";
|
|
2
|
+
import { isAnthropicOAuthInstance } from "../providers/anthropic-instance";
|
|
3
|
+
import { clearObservedAnthropicRefreshIntent } from "./anthropic-oauth-definitions";
|
|
4
|
+
import { logOAuthEvent } from "./log";
|
|
5
|
+
import { markOAuthRefreshIntentStaleOwner, readOAuthRefreshIntent } from "./store";
|
|
6
|
+
|
|
7
|
+
/** Independent of client lifetime; a cancelled caller can still be awaiting a shared refresh. */
|
|
8
|
+
export const MAX_OAUTH_TOKEN_REFRESH_WAITERS = 256;
|
|
9
|
+
export const tokenRefreshWaiters = createAdmissionGate("oauth_token_refresh_waiters", MAX_OAUTH_TOKEN_REFRESH_WAITERS);
|
|
10
|
+
|
|
11
|
+
export interface OAuthRefreshFlightEvidence { flightId: string; dispatched: boolean }
|
|
12
|
+
|
|
13
|
+
export interface RetirableOAuthRefreshFlight extends OAuthRefreshFlightEvidence {
|
|
14
|
+
settled?: boolean;
|
|
15
|
+
retirementDeferredLogged?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Retire only after the old attempt has finished its definitive-result cleanup. A provider
|
|
19
|
+
// may ignore abort; marking its intent sooner would poison a late, safely retryable rejection.
|
|
20
|
+
// Failed durable retirement retains the same bounded registry slot for a later retry.
|
|
21
|
+
export function retireSettledOAuthRefreshFlight<F extends RetirableOAuthRefreshFlight>(
|
|
22
|
+
tokenRefreshes: Map<string, F>, key: string, flight: F,
|
|
23
|
+
): boolean {
|
|
24
|
+
if (!flight.settled) return false;
|
|
25
|
+
const [provider, accountId] = key.split("\0");
|
|
26
|
+
const defer = (reason: "uncertain_intent" | "intent_changed" | "storage_failure"): false => {
|
|
27
|
+
if (!flight.retirementDeferredLogged) {
|
|
28
|
+
flight.retirementDeferredLogged = true;
|
|
29
|
+
logOAuthEvent("OAuth refresh flight retirement deferred", { provider: provider ?? "unknown", reason });
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
};
|
|
33
|
+
try {
|
|
34
|
+
if (provider && accountId && isAnthropicOAuthInstance(provider)) {
|
|
35
|
+
const intent = readOAuthRefreshIntent(provider, accountId);
|
|
36
|
+
if (intent?.uncertain) return defer("uncertain_intent");
|
|
37
|
+
if (intent?.flightId === flight.flightId && !intent.staleOwner && !intent.cleanupPending) {
|
|
38
|
+
const retired = flight.dispatched
|
|
39
|
+
? markOAuthRefreshIntentStaleOwner(provider, accountId, intent.generation, flight.flightId)
|
|
40
|
+
: clearObservedAnthropicRefreshIntent(provider, accountId, intent);
|
|
41
|
+
if (!retired) return defer("intent_changed");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch { return defer("storage_failure"); }
|
|
45
|
+
if (tokenRefreshes.get(key) === flight) tokenRefreshes.delete(key);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
/** Shared conversation identity precedence; Desktop cohort cache keys never bind sessions. */
|
|
4
|
+
export function oauthSessionKeyFromParts(input: {
|
|
5
|
+
sessionIdHeader?: string | null;
|
|
6
|
+
threadIdHeader?: string | null;
|
|
7
|
+
promptCacheKey?: string | null;
|
|
8
|
+
clientThreadId?: string | null;
|
|
9
|
+
/** When true, prompt_cache_key is a shared Desktop cohort — ignore it for affinity. */
|
|
10
|
+
promptCacheKeyIsSharedCohort?: boolean;
|
|
11
|
+
}): string | null {
|
|
12
|
+
const preferred = input.clientThreadId?.trim()
|
|
13
|
+
|| input.sessionIdHeader?.trim()
|
|
14
|
+
|| input.threadIdHeader?.trim()
|
|
15
|
+
|| "";
|
|
16
|
+
if (preferred) {
|
|
17
|
+
return preferred.length <= 128 ? preferred : createHash("sha256").update(preferred).digest("hex");
|
|
18
|
+
}
|
|
19
|
+
if (input.promptCacheKeyIsSharedCohort) return null;
|
|
20
|
+
const cacheKey = input.promptCacheKey?.trim() ?? "";
|
|
21
|
+
if (!cacheKey) return null;
|
|
22
|
+
return cacheKey.length <= 128 ? cacheKey : createHash("sha256").update(cacheKey).digest("hex");
|
|
23
|
+
}
|
|
@@ -32,6 +32,13 @@ export class AnthropicInstanceCollisionError extends Error {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export class AnthropicInstanceConfigReadError extends Error {
|
|
36
|
+
constructor() {
|
|
37
|
+
super("Cannot verify Anthropic Pool 2 ownership in the existing config file.");
|
|
38
|
+
this.name = "AnthropicInstanceConfigReadError";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
35
42
|
export function assertAnthropicInstanceLoginConfig(config: Pick<OcxConfig, "providers">, provider: string, credentialWritten = false): void {
|
|
36
43
|
if (provider !== "anthropic2") return;
|
|
37
44
|
const row = config.providers.anthropic2;
|
|
@@ -44,11 +51,11 @@ export function assertAnthropicInstanceLoginConfig(config: Pick<OcxConfig, "prov
|
|
|
44
51
|
try { raw = readFileSync(getConfigPath(), "utf8"); }
|
|
45
52
|
catch (error) {
|
|
46
53
|
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return;
|
|
47
|
-
throw new
|
|
54
|
+
throw new AnthropicInstanceConfigReadError();
|
|
48
55
|
}
|
|
49
56
|
let parsed: unknown;
|
|
50
57
|
try { parsed = JSON.parse(raw.replace(/^\uFEFF/, "")); }
|
|
51
|
-
catch { throw new
|
|
58
|
+
catch { throw new AnthropicInstanceConfigReadError(); }
|
|
52
59
|
const providers = parsed && typeof parsed === "object" && "providers" in parsed ? parsed.providers : undefined;
|
|
53
60
|
if (providers && typeof providers === "object" && Object.hasOwn(providers, provider)) {
|
|
54
61
|
const persisted = (providers as Record<string, unknown>)[provider];
|
package/src/oauth/store.ts
CHANGED
|
@@ -1028,8 +1028,8 @@ export async function saveCredential(
|
|
|
1028
1028
|
provider: string,
|
|
1029
1029
|
cred: OAuthCredentials,
|
|
1030
1030
|
opts: { preserveIdentityless?: boolean; assertBeforePersist?: () => void } = {},
|
|
1031
|
-
): Promise<
|
|
1032
|
-
await saveCredentialWithReceipt(provider, cred, opts);
|
|
1031
|
+
): Promise<OAuthCredentialWriteReceipt | null> {
|
|
1032
|
+
return await saveCredentialWithReceipt(provider, cred, opts);
|
|
1033
1033
|
}
|
|
1034
1034
|
|
|
1035
1035
|
/**
|
|
@@ -1234,23 +1234,29 @@ export function getAccountCredentialWithStatus(
|
|
|
1234
1234
|
};
|
|
1235
1235
|
}
|
|
1236
1236
|
|
|
1237
|
-
/**
|
|
1237
|
+
/**
|
|
1238
|
+
* Persist an explicit login/reauth credential for one account; runtime rotations use
|
|
1239
|
+
* mergeAccountCredential. Resolves false when nothing was written (the account is gone).
|
|
1240
|
+
*/
|
|
1238
1241
|
export async function saveAccountCredential(
|
|
1239
1242
|
provider: string,
|
|
1240
1243
|
accountId: string,
|
|
1241
1244
|
cred: OAuthCredentials,
|
|
1242
1245
|
opts: { assertBeforePersist?: () => void; rotateLoginId?: boolean } = {},
|
|
1243
|
-
): Promise<
|
|
1246
|
+
): Promise<boolean> {
|
|
1244
1247
|
if (isAnthropicInstanceId(provider)) assertAnthropicCredentialSource(provider, cred);
|
|
1245
1248
|
const safe = normalizeCredential(cred);
|
|
1246
|
-
if (!safe) return;
|
|
1249
|
+
if (!safe) return false;
|
|
1250
|
+
let written = false;
|
|
1247
1251
|
await mutateStore(store => {
|
|
1252
|
+
written = false;
|
|
1248
1253
|
if (provider === "anthropic2") assertAnthropicInstanceLoginConfig(loadConfig(), provider);
|
|
1249
1254
|
if (isAnthropicInstanceId(provider)) assertNoCrossAnthropicRegistration(store, provider, safe);
|
|
1250
1255
|
const set = store[provider];
|
|
1251
1256
|
const account = set?.accounts.find(a => a.id === accountId);
|
|
1252
1257
|
if (!set || !account) return;
|
|
1253
1258
|
account.credential = safe;
|
|
1259
|
+
written = true;
|
|
1254
1260
|
if (opts.rotateLoginId) account.loginId = randomUUID();
|
|
1255
1261
|
delete account.needsReauth;
|
|
1256
1262
|
delete account.needsReauthReason;
|
|
@@ -1263,6 +1269,7 @@ export async function saveAccountCredential(
|
|
|
1263
1269
|
set.selectionRevision = randomUUID();
|
|
1264
1270
|
}
|
|
1265
1271
|
}, [provider, accountId, safe], { assertBeforePersist: registrationPersistGuard(provider, opts.assertBeforePersist) });
|
|
1272
|
+
return written;
|
|
1266
1273
|
}
|
|
1267
1274
|
|
|
1268
1275
|
function accountSelection(set: ProviderAccountSet): OAuthAccountSelection {
|
|
@@ -61,6 +61,7 @@ export interface ClientWebSearch {
|
|
|
61
61
|
status: "completed" | "failed";
|
|
62
62
|
queries: string[];
|
|
63
63
|
sources: { url: string; title?: string }[];
|
|
64
|
+
fetch?: { url: string; result: unknown };
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
/**
|
|
@@ -411,9 +412,9 @@ export function encodeAdapterEventStream(
|
|
|
411
412
|
budget.closeCall(call.callId);
|
|
412
413
|
currentToolCall = null;
|
|
413
414
|
};
|
|
414
|
-
const closeCurrentWebSearch = (status: "completed" | "failed", queries: string[], sources?: { url: string; title?: string }[]) => {
|
|
415
|
+
const closeCurrentWebSearch = (status: "completed" | "failed", queries: string[], sources?: { url: string; title?: string }[], fetch?: ClientWebSearch["fetch"]) => {
|
|
415
416
|
if (!currentWebSearch) return;
|
|
416
|
-
writer.webSearchDone({ itemId: currentWebSearch.itemId, status, queries, sources: sources ?? [] });
|
|
417
|
+
writer.webSearchDone({ itemId: currentWebSearch.itemId, status, queries, sources: sources ?? [], ...(fetch ? { fetch } : {}) });
|
|
417
418
|
currentWebSearch = null;
|
|
418
419
|
};
|
|
419
420
|
const appendToolArgs = (call: OpenToolCall, fragment: string) => {
|
|
@@ -688,7 +689,7 @@ export function encodeAdapterEventStream(
|
|
|
688
689
|
if (currentWebSearch) closeCurrentWebSearch("completed", []);
|
|
689
690
|
currentWebSearch = { itemId: `ws_${uuid()}`, eventId: event.id };
|
|
690
691
|
}
|
|
691
|
-
closeCurrentWebSearch(event.status ?? "completed", event.queries, safeWebSearchSources(event.sources));
|
|
692
|
+
closeCurrentWebSearch(event.status ?? "completed", event.queries, safeWebSearchSources(event.sources), event.fetch);
|
|
692
693
|
break;
|
|
693
694
|
}
|
|
694
695
|
case "done": {
|
|
@@ -729,7 +730,7 @@ export function encodeAdapterEventStream(
|
|
|
729
730
|
terminal = {
|
|
730
731
|
status: "incomplete",
|
|
731
732
|
incomplete: {
|
|
732
|
-
reason: event.reason,
|
|
733
|
+
reason: truncationReasonFor(event.reason) ?? event.reason,
|
|
733
734
|
...(event.message ? { message: event.message } : {}),
|
|
734
735
|
...(event.retryable !== undefined ? { retryable: event.retryable } : {}),
|
|
735
736
|
},
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type TranslatorBudget,
|
|
17
17
|
} from "../../lib/translator-budget";
|
|
18
18
|
import { isTransientUpstreamStatus } from "../../lib/upstream-retry";
|
|
19
|
+
import { UNDECLARED_TOOL_CALL_ERROR_CODE } from "../../lib/errors";
|
|
19
20
|
import { encodeReasoningEnvelope } from "../../responses/reasoning-envelope";
|
|
20
21
|
import { responsesUsage, webSearchAction } from "../../bridge/internal";
|
|
21
22
|
import {
|
|
@@ -344,13 +345,14 @@ function createAnthropicMessageWriter(
|
|
|
344
345
|
type: "web_search_call",
|
|
345
346
|
id: search.itemId,
|
|
346
347
|
status: search.status,
|
|
347
|
-
action: webSearchAction(search.queries),
|
|
348
|
+
action: webSearchAction(search.queries, search.fetch),
|
|
348
349
|
...(search.sources.length > 0 ? { sources: search.sources } : {}),
|
|
350
|
+
...(search.fetch ? { fetch_result: search.fetch.result } : {}),
|
|
349
351
|
});
|
|
350
352
|
const toolIndex = blockIndex++;
|
|
351
353
|
emit("content_block_start", {
|
|
352
354
|
type: "content_block_start", index: toolIndex,
|
|
353
|
-
content_block: { type: "server_tool_use", id: pair.id, name:
|
|
355
|
+
content_block: { type: "server_tool_use", id: pair.id, name: pair.name },
|
|
354
356
|
}, { sideEffect: true });
|
|
355
357
|
emit("content_block_delta", {
|
|
356
358
|
type: "content_block_delta", index: toolIndex,
|
|
@@ -360,10 +362,10 @@ function createAnthropicMessageWriter(
|
|
|
360
362
|
const resultIndex = blockIndex++;
|
|
361
363
|
emit("content_block_start", {
|
|
362
364
|
type: "content_block_start", index: resultIndex,
|
|
363
|
-
content_block: { type:
|
|
365
|
+
content_block: { type: pair.resultType, tool_use_id: pair.id, content: pair.resultContent },
|
|
364
366
|
});
|
|
365
367
|
emit("content_block_stop", { type: "content_block_stop", index: resultIndex });
|
|
366
|
-
if (pair.completed) webSearchRequests++;
|
|
368
|
+
if (pair.completed && pair.name === "web_search") webSearchRequests++;
|
|
367
369
|
},
|
|
368
370
|
terminal(terminal) {
|
|
369
371
|
if (terminal.status === "completed") {
|
|
@@ -386,8 +388,9 @@ function createAnthropicMessageWriter(
|
|
|
386
388
|
return;
|
|
387
389
|
}
|
|
388
390
|
const message = typeof error.message === "string" ? error.message : "upstream request failed";
|
|
389
|
-
|
|
390
|
-
|
|
391
|
+
const deterministic = error.code === UNDECLARED_TOOL_CALL_ERROR_CODE;
|
|
392
|
+
fail(anthropicFailedStatus(error, message), message, !deterministic,
|
|
393
|
+
deterministic || error.code === "context_length_exceeded" ? error.code as string : undefined);
|
|
391
394
|
},
|
|
392
395
|
overflow() {
|
|
393
396
|
fail(413, "upstream translation buffer exceeded the safe limit", false, "translation_buffer_limit");
|
|
@@ -55,12 +55,13 @@ function usableModelId(model: unknown): model is string {
|
|
|
55
55
|
function candidateModelIds(name: string, provider: Readonly<OcxProviderConfig>): string[] {
|
|
56
56
|
const registry = PROVIDER_REGISTRY.find(entry => entry.id === name);
|
|
57
57
|
const ids = new Set<string>();
|
|
58
|
+
/** Collect valid candidate IDs without exceeding the protocol-summary scan limit. */
|
|
58
59
|
const add = (model: unknown) => {
|
|
59
60
|
if (ids.size < MODEL_SCAN_LIMIT && usableModelId(model)) ids.add(model);
|
|
60
61
|
};
|
|
61
62
|
for (const model of Object.keys(provider.modelAdapters ?? {})) add(model);
|
|
62
63
|
for (const model of Object.keys(registry?.modelWireDefaults ?? {})) add(model);
|
|
63
|
-
for (const model of Object.keys(captureWireAdapterHardPins(name))) add(model);
|
|
64
|
+
for (const model of Object.keys(captureWireAdapterHardPins(name, provider))) add(model);
|
|
64
65
|
add(provider.defaultModel);
|
|
65
66
|
for (const model of provider.models ?? []) add(model);
|
|
66
67
|
return [...ids].sort((left, right) => left.localeCompare(right));
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Process-local cooldown state shared by selection validation and key failover. */
|
|
2
|
+
const keyCooldowns = new Map<string, { cooldownUntil: number }>();
|
|
3
|
+
|
|
4
|
+
function cooldownKey(providerName: string, keyId: string): string {
|
|
5
|
+
return `${providerName}\0${keyId}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function setKeyCooldown(providerName: string, keyId: string, cooldownUntil: number): void {
|
|
9
|
+
const key = cooldownKey(providerName, keyId);
|
|
10
|
+
keyCooldowns.set(key, { cooldownUntil: Math.max(keyCooldowns.get(key)?.cooldownUntil ?? 0, cooldownUntil) });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isKeyInCooldown(providerName: string, keyId: string, now = Date.now()): boolean {
|
|
14
|
+
const entry = keyCooldowns.get(cooldownKey(providerName, keyId));
|
|
15
|
+
if (!entry) return false;
|
|
16
|
+
if (entry.cooldownUntil <= now) {
|
|
17
|
+
keyCooldowns.delete(cooldownKey(providerName, keyId));
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getKeyCooldownUntil(providerName: string, keyId: string, now = Date.now()): number | null {
|
|
24
|
+
const entry = keyCooldowns.get(cooldownKey(providerName, keyId));
|
|
25
|
+
if (!entry) return null;
|
|
26
|
+
return entry.cooldownUntil > now ? entry.cooldownUntil : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function sweepExpiredApiKeyCooldowns(now = Date.now()): number {
|
|
30
|
+
let removed = 0;
|
|
31
|
+
for (const [key, cooldown] of keyCooldowns) {
|
|
32
|
+
if (cooldown.cooldownUntil > now) continue;
|
|
33
|
+
keyCooldowns.delete(key);
|
|
34
|
+
removed += 1;
|
|
35
|
+
}
|
|
36
|
+
return removed;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Manual key management clears this provider's cooldowns. */
|
|
40
|
+
export function clearKeyCooldowns(providerName?: string): void {
|
|
41
|
+
if (!providerName) {
|
|
42
|
+
keyCooldowns.clear();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const prefix = `${providerName}\0`;
|
|
46
|
+
for (const key of keyCooldowns.keys()) {
|
|
47
|
+
if (key.startsWith(prefix)) keyCooldowns.delete(key);
|
|
48
|
+
}
|
|
49
|
+
}
|