@bitkyc08/opencodex 2.76.0 → 2.78.0-preview.20261005
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ocx.mjs +11 -10
- package/gui/dist/assets/App-CppMz-6w.js +54 -0
- package/gui/dist/assets/App-lCIslZ1L.css +1 -0
- package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
- package/gui/dist/assets/index-DQt9srNi.js +94 -0
- package/gui/dist/assets/index-DmQB8D_R.css +1 -0
- package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
- package/gui/dist/index.html +4 -3
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +19 -2
- package/src/adapters/anthropic/client-identity.ts +13 -4
- package/src/adapters/anthropic/native-client-preamble.ts +31 -0
- package/src/adapters/anthropic/passthrough.ts +103 -25
- package/src/adapters/antigravity-refusal.ts +28 -0
- package/src/adapters/cursor/protobuf-request.ts +6 -0
- package/src/adapters/cursor/request-builder.ts +3 -0
- package/src/adapters/cursor/structured-output.ts +13 -0
- package/src/adapters/cursor/types.ts +2 -0
- package/src/adapters/devin/cloud-direct/chat.ts +7 -2
- package/src/adapters/devin/trajectory.ts +49 -0
- package/src/adapters/devin.ts +13 -0
- package/src/adapters/ollama-native.ts +62 -19
- package/src/adapters/openai-responses/passthrough.ts +133 -103
- package/src/adapters/openai-responses/request-strips.ts +10 -4
- package/src/adapters/zed.ts +26 -4
- package/src/bridge/response-json.ts +14 -14
- package/src/bridge/sse.ts +15 -5
- package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
- package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
- package/src/claude/agents-inject.ts +4 -43
- package/src/claude/gateway-cache.ts +16 -13
- package/src/claude/inbound-model-options.ts +9 -2
- package/src/claude/intercept/cli-catalog.ts +27 -16
- package/src/claude/intercept/client-hello.ts +103 -0
- package/src/claude/intercept/model-bindings.ts +3 -2
- package/src/claude/intercept/picker-bootstrap.ts +37 -16
- package/src/claude/intercept/picker-budget.ts +41 -0
- package/src/claude/intercept/picker-listener.ts +279 -66
- package/src/claude/outbound.ts +2 -0
- package/src/claude/subagent-force-status.ts +53 -0
- package/src/claude/subagent-model.ts +93 -0
- package/src/cli/access-audio-input.ts +84 -0
- package/src/cli/access-audio-live.ts +99 -0
- package/src/cli/access-audio.ts +123 -0
- package/src/cli/access-data-client.ts +127 -0
- package/src/cli/access-data-plane.ts +104 -0
- package/src/cli/access-data-response.ts +76 -0
- package/src/cli/access.ts +65 -2
- package/src/cli/account-api.ts +33 -6
- package/src/cli/account-auth.ts +111 -42
- package/src/cli/account-key-quota.ts +118 -0
- package/src/cli/account-main.ts +11 -4
- package/src/cli/account-next-actions.ts +63 -0
- package/src/cli/account-policy-dto.ts +68 -0
- package/src/cli/account-policy.ts +167 -0
- package/src/cli/account-target.ts +8 -0
- package/src/cli/account.ts +37 -9
- package/src/cli/agent-runtime-settings.ts +130 -0
- package/src/cli/agent-settings.ts +101 -0
- package/src/cli/agent.ts +29 -2
- package/src/cli/alias.ts +7 -2
- package/src/cli/aside-profiles.ts +2 -2
- package/src/cli/capabilities-access-remote.ts +147 -0
- package/src/cli/capabilities-accounts.ts +195 -0
- package/src/cli/capabilities-agents-routing.ts +414 -0
- package/src/cli/capabilities-base.ts +783 -0
- package/src/cli/capabilities-command.ts +1 -0
- package/src/cli/capabilities-integrations.ts +264 -0
- package/src/cli/capabilities-lab.ts +214 -0
- package/src/cli/capabilities-observe-system.ts +662 -0
- package/src/cli/capabilities-provider-models.ts +302 -0
- package/src/cli/capabilities.ts +30 -1190
- package/src/cli/capability-types.ts +54 -0
- package/src/cli/catalog-command-result.ts +57 -0
- package/src/cli/chatgpt-command.ts +35 -9
- package/src/cli/claude-agent-startup-sync.ts +17 -0
- package/src/cli/claude-desktop-profile.ts +100 -0
- package/src/cli/claude-desktop.ts +39 -6
- package/src/cli/claude.ts +22 -6
- package/src/cli/codex-shim-autorestore.ts +2 -1
- package/src/cli/codex-shim-readiness.ts +109 -0
- package/src/cli/combo-input.ts +88 -0
- package/src/cli/combo-result.ts +52 -0
- package/src/cli/combo-stats.ts +123 -0
- package/src/cli/combo.ts +37 -14
- package/src/cli/companion-timeline.ts +142 -0
- package/src/cli/companion-usage.ts +146 -0
- package/src/cli/companion.ts +12 -3
- package/src/cli/config-command.ts +24 -8
- package/src/cli/connect.ts +19 -0
- package/src/cli/dispatch.ts +55 -82
- package/src/cli/doctor.ts +22 -4
- package/src/cli/export-command.ts +20 -9
- package/src/cli/gui.ts +8 -3
- package/src/cli/help-catalog.ts +101 -0
- package/src/cli/help-models-context.ts +17 -0
- package/src/cli/help-navigation.ts +50 -0
- package/src/cli/help-recovery.ts +66 -0
- package/src/cli/help.ts +69 -11
- package/src/cli/index.ts +38 -27
- package/src/cli/injection-follow.ts +35 -0
- package/src/cli/inspect.ts +5 -0
- package/src/cli/integration-aside-sync.ts +69 -0
- package/src/cli/integration-cursor.ts +106 -0
- package/src/cli/integration-input.ts +60 -0
- package/src/cli/integration-journal.ts +49 -0
- package/src/cli/integration-plan-dto.ts +156 -0
- package/src/cli/integration-preview.ts +215 -0
- package/src/cli/integrations.ts +24 -18
- package/src/cli/json-input.ts +134 -0
- package/src/cli/link.ts +32 -4
- package/src/cli/local-sync-result.ts +41 -0
- package/src/cli/log-follow.ts +84 -0
- package/src/cli/log-view-filter.ts +146 -0
- package/src/cli/logout-command.ts +44 -0
- package/src/cli/model-picker-ordering.ts +137 -0
- package/src/cli/models-custom-input.ts +107 -0
- package/src/cli/models-custom-runtime.ts +112 -0
- package/src/cli/models-order.ts +194 -0
- package/src/cli/models-runtime-subcommands.ts +2 -0
- package/src/cli/models-runtime.ts +9 -1
- package/src/cli/models.ts +66 -81
- package/src/cli/observe-stream.ts +142 -0
- package/src/cli/observe.ts +75 -34
- package/src/cli/opencode.ts +54 -10
- package/src/cli/provider-batch.ts +77 -0
- package/src/cli/provider-lifecycle-runtime.ts +170 -0
- package/src/cli/provider-result.ts +124 -0
- package/src/cli/provider-runtime.ts +15 -12
- package/src/cli/provider-settings.ts +143 -0
- package/src/cli/provider.ts +116 -64
- package/src/cli/registry.ts +90 -24
- package/src/cli/remote-workspace-hub.ts +83 -0
- package/src/cli/remote-workspace.ts +6 -1
- package/src/cli/restart-failure.ts +27 -0
- package/src/cli/root.ts +42 -9
- package/src/cli/route-policy-write.ts +137 -0
- package/src/cli/route-policy.ts +8 -0
- package/src/cli/runtime-api.ts +57 -80
- package/src/cli/secret-args.ts +149 -0
- package/src/cli/settings-result.ts +30 -0
- package/src/cli/storage.ts +77 -7
- package/src/cli/system-command.ts +60 -2
- package/src/cli/system-restart-client.ts +20 -3
- package/src/cli/system-settings-parity.ts +83 -0
- package/src/cli/tray-proxy.ts +3 -2
- package/src/cli/update-restart-candidate.ts +21 -0
- package/src/cli/update-restart-child.ts +81 -0
- package/src/cli/update-restart-home.ts +38 -0
- package/src/cli/update-restart-transport.ts +193 -0
- package/src/cli/update-restart.ts +260 -0
- package/src/cli/usage-model-search.ts +58 -0
- package/src/cli/usage-report.ts +13 -5
- package/src/cli/v2-input.ts +66 -0
- package/src/cli/v2-local-output.ts +37 -0
- package/src/cli/v2-runtime.ts +90 -0
- package/src/cli/v2.ts +102 -144
- package/src/clients/config-export/contracts.ts +100 -0
- package/src/clients/config-export/droid.ts +54 -2
- package/src/clients/config-export/dsh.ts +21 -0
- package/src/clients/config-export/kilo.ts +12 -2
- package/src/clients/config-export/model-metadata.ts +10 -4
- package/src/clients/config-export/reasoning-metadata.ts +47 -0
- package/src/clients/config-export.ts +53 -67
- package/src/clients/omo-role-models.ts +17 -3
- package/src/codex/account-credit-use.ts +4 -2
- package/src/codex/account-runtime-state.ts +21 -0
- package/src/codex/account-store.ts +11 -47
- package/src/codex/account-usability.ts +11 -8
- package/src/codex/auth-api/account-list.ts +73 -63
- package/src/codex/auth-api/main-account-probe.ts +25 -12
- package/src/codex/auth-api/pool-mode-gate.ts +6 -9
- package/src/codex/auth-api/pool-quota-probe.ts +2 -4
- package/src/codex/auth-api/reset-credit-service.ts +3 -2
- package/src/codex/auth-context.ts +30 -11
- package/src/codex/catalog/aggregation.ts +3 -0
- package/src/codex/catalog/build-entries.ts +1 -1
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/heal-observation.ts +66 -0
- package/src/codex/catalog/model-hints.ts +2 -1
- package/src/codex/catalog/model-visibility.ts +6 -3
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +13 -4
- package/src/codex/catalog/publication-observer.ts +19 -0
- package/src/codex/catalog/remote.ts +13 -15
- package/src/codex/catalog/restore.ts +19 -6
- package/src/codex/catalog/retained-sync.ts +60 -11
- package/src/codex/catalog/routed-removal.ts +116 -0
- package/src/codex/catalog/write-audit-contract.ts +21 -0
- package/src/codex/catalog/write-audit.ts +171 -0
- package/src/codex/catalog-self-heal.ts +284 -0
- package/src/codex/catalog-write-serialization.ts +71 -7
- package/src/codex/chatgpt-refresh-failure.ts +56 -0
- package/src/codex/codex-home-owner.ts +124 -0
- package/src/codex/convergence.ts +57 -16
- package/src/codex/features.ts +8 -1
- package/src/codex/inject/restore.ts +46 -4
- package/src/codex/inject.ts +11 -1
- package/src/codex/internal/catalog-writer.ts +120 -13
- package/src/codex/journal.ts +49 -13
- package/src/codex/main-account.ts +144 -11
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/quota-refresh-outcome.ts +17 -2
- package/src/codex/quota-types.ts +36 -1
- package/src/codex/quota.ts +67 -16
- package/src/codex/refresh.ts +20 -9
- package/src/codex/routing/cooldown-math.ts +10 -4
- package/src/codex/routing/selection.ts +11 -4
- package/src/codex/routing.ts +9 -9
- package/src/codex/runtime.ts +7 -1
- package/src/codex/shim-diagnostics.ts +80 -0
- package/src/codex/shim-fingerprint.ts +7 -2
- package/src/codex/shim-migration.ts +153 -0
- package/src/codex/shim-overlay.ts +509 -0
- package/src/codex/shim-path-resolution.ts +145 -0
- package/src/codex/shim-probe.ts +4 -3
- package/src/codex/shim-state-file.ts +93 -7
- package/src/codex/shim.ts +54 -107
- package/src/codex/subagent-model-fallback.ts +2 -1
- package/src/codex/sync.ts +24 -4
- package/src/combos/failover.ts +101 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/jev.ts +13 -3
- package/src/combos/resolve.ts +12 -0
- package/src/config/atomic-write.ts +13 -8
- package/src/config/diagnostics.ts +23 -1
- package/src/config/load-degrade.ts +7 -0
- package/src/config/provider-validation.ts +3 -0
- package/src/config/schema/chatgpt-desktop.ts +22 -0
- package/src/config/schema/compaction-triggers.ts +8 -0
- package/src/config/schema/config-schema.ts +43 -14
- package/src/config/schema/leaf-validators.ts +8 -3
- package/src/config/subagent-models.ts +12 -0
- package/src/generated/compatibility-version.json +645 -237
- package/src/grok/session-identity.ts +33 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/merge.ts +13 -5
- package/src/integrations/mutation-plan.ts +50 -5
- package/src/integrations/omp-yaml-source.ts +181 -1
- package/src/integrations/ownership-policy.ts +66 -0
- package/src/integrations/registry.ts +50 -1
- package/src/integrations/state.ts +106 -19
- package/src/integrations/target.ts +25 -2
- package/src/integrations/writer.ts +107 -35
- package/src/lib/gui-pair-capability.ts +17 -0
- package/src/lib/package-tree-integrity.ts +42 -1
- package/src/lib/provider-client-headers.ts +62 -0
- package/src/lib/proxy-env.ts +3 -0
- package/src/lib/test-home-guard.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +19 -8
- package/src/oauth/anthropic-continuity.ts +65 -0
- package/src/oauth/anthropic-identity.ts +75 -0
- package/src/oauth/anthropic-routing.ts +20 -15
- package/src/oauth/anthropic.ts +34 -3
- package/src/oauth/generic-account-failover.ts +21 -0
- package/src/oauth/health.ts +32 -5
- package/src/oauth/index.ts +42 -35
- package/src/oauth/local-token-detect.ts +7 -0
- package/src/oauth/pool-settings-capability.ts +9 -2
- package/src/oauth/refresh-policy.ts +2 -0
- package/src/oauth/store.ts +55 -6
- package/src/oauth/types.ts +9 -0
- package/src/protocols/encoders/messages.ts +4 -3
- package/src/protocols/plan-snapshot.ts +1 -1
- package/src/protocols/settings.ts +28 -4
- package/src/providers/antigravity-effort-families.ts +75 -0
- package/src/providers/antigravity-models.ts +54 -16
- package/src/providers/antigravity-wire-snapshot.ts +66 -0
- package/src/providers/derive.ts +2 -0
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/new-model-policy.ts +48 -10
- package/src/providers/registry/entries-extended.ts +3 -0
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/model-seeds.ts +5 -0
- package/src/providers/registry/types.ts +4 -0
- package/src/responses/compaction.ts +19 -0
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +9 -0
- package/src/server/caller-session-identity.ts +32 -0
- package/src/server/chat-completions.ts +10 -0
- package/src/server/claude-messages.ts +29 -15
- package/src/server/droid-reasoning-default.ts +50 -0
- package/src/server/github-copilot-responses-repair.ts +3 -6
- package/src/server/grok-responses-control-frame.ts +2 -2
- package/src/server/grok-responses-snapshot-repair.ts +4 -5
- package/src/server/gui-session.ts +33 -11
- package/src/server/index/claude-intercept-lifecycle.ts +4 -0
- package/src/server/index/serve-options.ts +13 -7
- package/src/server/index.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +12 -134
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +8 -1
- package/src/server/management/cursor-integration-routes.ts +3 -3
- package/src/server/management/integration-routes.ts +69 -11
- package/src/server/management/link-routes.ts +20 -13
- package/src/server/management/model-routes.ts +3 -3
- package/src/server/management/model-row-export-metadata.ts +150 -0
- package/src/server/management/model-rows.ts +148 -9
- package/src/server/management/native-integration-routes.ts +2 -2
- package/src/server/management/oauth-account-routes.ts +96 -30
- package/src/server/management/route-registry.ts +29 -27
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/subagent-model-routes.ts +214 -0
- package/src/server/messages-native-eligibility.ts +3 -6
- package/src/server/messages-native-oauth.ts +82 -35
- package/src/server/messages-native.ts +78 -16
- package/src/server/relay.ts +14 -44
- package/src/server/request-log-generation-window.ts +38 -0
- package/src/server/request-log.ts +13 -0
- package/src/server/responses/adapter-dispatch.ts +152 -28
- package/src/server/responses/codex-auth-error.ts +10 -2
- package/src/server/responses/combo-stream-preflight.ts +29 -3
- package/src/server/responses/compaction-routing.ts +10 -1
- package/src/server/responses/core-auth.ts +67 -30
- package/src/server/responses/core-codex-account.ts +6 -3
- package/src/server/responses/core-combo-failure.ts +5 -1
- package/src/server/responses/core-combo-native.ts +7 -1
- package/src/server/responses/core-combo.ts +64 -7
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/fetch-helpers.ts +9 -0
- package/src/server/responses/passthrough-delivery.ts +1 -1
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/request-prepare.ts +5 -0
- package/src/server/responses/request-spend.ts +30 -30
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/responses-hosted-image-display.ts +70 -9
- package/src/server/responses-terminal-repair.ts +15 -2
- package/src/server/sse-frame-buffer.ts +38 -36
- package/src/server/sse-payload-rewrite.ts +78 -49
- package/src/server/ws-bridge.ts +1 -11
- package/src/service/cli.ts +6 -1
- package/src/service/ownership-mutation-lease.d.mts +26 -0
- package/src/service/ownership-mutation-lease.mjs +76 -1
- package/src/service/windows-taskxml.ts +1 -2
- package/src/service-manager-probe.ts +36 -2
- package/src/types/config.ts +6 -0
- package/src/types/provider.ts +28 -10
- package/src/update/index.ts +2 -1
- package/src/update/npm-invocation.mjs +35 -3
- package/src/update/proxy-liveness-probe.mjs +38 -8
- package/src/update/update-failure-guidance.d.mts +6 -0
- package/src/update/update-failure-guidance.mjs +30 -5
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +7 -0
- package/src/usage/summary.ts +82 -1
- package/src/web-search/loop.ts +11 -8
- package/src/web-search/run-turn-loop.ts +10 -7
- package/gui/dist/assets/App-CddorHdA.js +0 -51
- package/gui/dist/assets/App-DMudtISW.css +0 -1
- package/gui/dist/assets/index-Btze1I3T.css +0 -1
- package/gui/dist/assets/index-CvnY7E6i.js +0 -86
- package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { isRealBunBinary } from "./bun-binary-validator.mjs";
|
|
4
5
|
import { isStandaloneBinary } from "./standalone";
|
|
5
6
|
|
|
6
7
|
export interface PackageTreeObservation {
|
|
@@ -56,6 +57,14 @@ export interface PackageTreeIntegrityOptions {
|
|
|
56
57
|
schedule?: (callback: () => void, delayMs: number) => (() => void) | void;
|
|
57
58
|
/** Test seam for `installedVersion()`; production reads the package manifest. */
|
|
58
59
|
readInstalledVersion?: () => string | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* Whether the runtime a restart would spawn is complete. An in-place npm install writes
|
|
62
|
+
* package.json and extracts the `bun` package's small placeholder before its postinstall
|
|
63
|
+
* swaps the real binary in, and the replacement spawns `process.execPath`, that same path.
|
|
64
|
+
* While this answers false the replacement is treated like an unreadable tree: no restart,
|
|
65
|
+
* then a fresh full debounce once the runtime is ready.
|
|
66
|
+
*/
|
|
67
|
+
runtimeReady?: () => boolean;
|
|
59
68
|
}
|
|
60
69
|
|
|
61
70
|
export type ObservePackageTree = () => PackageTreeObservation | null;
|
|
@@ -142,6 +151,12 @@ export function sameObservation(left: PackageTreeObservation, right: PackageTree
|
|
|
142
151
|
*/
|
|
143
152
|
const PACKAGE_TREE_RECHECK_MS = 1_000;
|
|
144
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Fences a live process whose package manifest was replaced, and once the replacement is readable,
|
|
156
|
+
* stable for the debounce interval and any configured `runtimeReady` check passes, hands it to
|
|
157
|
+
* `onReplaced`. If the callback throws, the guard rechecks the tree and may call it again after
|
|
158
|
+
* another debounce interval while the replacement persists.
|
|
159
|
+
*/
|
|
145
160
|
export function createPackageTreeIntegrityGuard(
|
|
146
161
|
observe: ObservePackageTree = observePackageManifest,
|
|
147
162
|
now: () => number = Date.now,
|
|
@@ -154,6 +169,8 @@ export function createPackageTreeIntegrityGuard(
|
|
|
154
169
|
let timerScheduled = false;
|
|
155
170
|
let cancelScheduled: (() => void) | null = null;
|
|
156
171
|
let waitingForReadableTree = false;
|
|
172
|
+
/** Logged once per wait, so a postinstall that never runs is visible rather than silent. */
|
|
173
|
+
let waitingForRuntime = false;
|
|
157
174
|
let replacementCandidate: PackageTreeObservation | null = null;
|
|
158
175
|
/** The replacement identity that survived a full stability interval (see installedVersion). */
|
|
159
176
|
let settledReplacement: PackageTreeObservation | null = null;
|
|
@@ -192,9 +209,24 @@ export function createPackageTreeIntegrityGuard(
|
|
|
192
209
|
if (sameObservation(boot, current)) {
|
|
193
210
|
resetRestartTimer();
|
|
194
211
|
waitingForReadableTree = false;
|
|
212
|
+
waitingForRuntime = false;
|
|
195
213
|
replacementCandidate = null;
|
|
196
214
|
return;
|
|
197
215
|
}
|
|
216
|
+
if (options.runtimeReady && !options.runtimeReady()) {
|
|
217
|
+
if (!waitingForRuntime) {
|
|
218
|
+
waitingForRuntime = true;
|
|
219
|
+
console.warn("Package tree replaced; waiting for its Bun runtime to finish installing before restarting");
|
|
220
|
+
}
|
|
221
|
+
// A replacement settled before a failed admission is no longer restart-ready: withdraw
|
|
222
|
+
// it so installedVersion() stays silent until the fresh debounce settles it again.
|
|
223
|
+
settledReplacement = null;
|
|
224
|
+
resetRestartTimer();
|
|
225
|
+
waitingForReadableTree = true;
|
|
226
|
+
armRestartTimer(PACKAGE_TREE_RECHECK_MS);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
waitingForRuntime = false;
|
|
198
230
|
if (waitingForReadableTree) {
|
|
199
231
|
waitingForReadableTree = false;
|
|
200
232
|
replacementCandidate = current;
|
|
@@ -239,6 +271,7 @@ export function createPackageTreeIntegrityGuard(
|
|
|
239
271
|
return {
|
|
240
272
|
installedVersion: () => {
|
|
241
273
|
if (settledReplacement === null) return undefined;
|
|
274
|
+
if (options.runtimeReady && !options.runtimeReady()) return undefined;
|
|
242
275
|
const current = observe();
|
|
243
276
|
if (current === null || !sameObservation(settledReplacement, current)) return undefined;
|
|
244
277
|
return readInstalledVersion();
|
|
@@ -271,6 +304,7 @@ export function createPackageTreeIntegrityGuard(
|
|
|
271
304
|
lastOkAt = at;
|
|
272
305
|
resetRestartTimer();
|
|
273
306
|
waitingForReadableTree = false;
|
|
307
|
+
waitingForRuntime = false;
|
|
274
308
|
replacementCandidate = null;
|
|
275
309
|
return { ok: true };
|
|
276
310
|
},
|
|
@@ -292,5 +326,12 @@ export function createRuntimePackageTreeIntegrityGuard(
|
|
|
292
326
|
if (installer === "source" || isStandaloneBinary()) {
|
|
293
327
|
return { status: () => ({ ok: true }), dispose: () => {} };
|
|
294
328
|
}
|
|
295
|
-
|
|
329
|
+
// The restart replacement spawns process.execPath, the bundled Bun in an npm install. Read it
|
|
330
|
+
// now: Linux Bun resolves it lazily, and after npm swaps the package directory a first read
|
|
331
|
+
// reports the retired binary as "(deleted)"; reading it once pins the boot path.
|
|
332
|
+
const runtimePath = process.execPath;
|
|
333
|
+
return createPackageTreeIntegrityGuard(observe, now, {
|
|
334
|
+
...options,
|
|
335
|
+
runtimeReady: options.runtimeReady ?? (() => isRealBunBinary(runtimePath)),
|
|
336
|
+
});
|
|
296
337
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
2
|
+
const BLOCKED_FORWARDED_CLIENT_HEADERS = new Set([
|
|
3
|
+
"authorization",
|
|
4
|
+
"chatgpt-account-id",
|
|
5
|
+
"connection",
|
|
6
|
+
"content-length",
|
|
7
|
+
"content-type",
|
|
8
|
+
"cookie",
|
|
9
|
+
"expect",
|
|
10
|
+
"host",
|
|
11
|
+
"keep-alive",
|
|
12
|
+
"proxy-authorization",
|
|
13
|
+
"proxy-connection",
|
|
14
|
+
"set-cookie",
|
|
15
|
+
"te",
|
|
16
|
+
"trailer",
|
|
17
|
+
"transfer-encoding",
|
|
18
|
+
"upgrade",
|
|
19
|
+
"x-amz-security-token",
|
|
20
|
+
"x-api-key",
|
|
21
|
+
"api-key",
|
|
22
|
+
"x-goog-api-key",
|
|
23
|
+
"x-oai-attestation",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const ALLOWED_FORWARDED_CLIENT_HEADERS = new Set([
|
|
27
|
+
"originator", "x-client-request-id", "x-codex-app-version", "user-agent",
|
|
28
|
+
]);
|
|
29
|
+
const MAX_FORWARDED_CLIENT_HEADERS = 64;
|
|
30
|
+
|
|
31
|
+
/** Normalize one opt-in caller header while refusing credential and transport-owned fields. */
|
|
32
|
+
export function normalizeForwardedClientHeaderName(value: unknown): string | null {
|
|
33
|
+
if (typeof value !== "string") return null;
|
|
34
|
+
const name = value.trim().toLowerCase();
|
|
35
|
+
if (!name || !HEADER_NAME_PATTERN.test(name) || !ALLOWED_FORWARDED_CLIENT_HEADERS.has(name)) return null;
|
|
36
|
+
return name;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Validate provider.forwardClientHeaders without ever admitting credential-bearing headers. */
|
|
40
|
+
export function providerForwardClientHeadersConfigError(value: unknown): string | null {
|
|
41
|
+
if (value === undefined) return null;
|
|
42
|
+
if (!Array.isArray(value)) return "forwardClientHeaders must be an array of header names";
|
|
43
|
+
if (value.length > MAX_FORWARDED_CLIENT_HEADERS) {
|
|
44
|
+
return `forwardClientHeaders must contain at most ${MAX_FORWARDED_CLIENT_HEADERS} names`;
|
|
45
|
+
}
|
|
46
|
+
const seen = new Set<string>();
|
|
47
|
+
for (const raw of value) {
|
|
48
|
+
if (typeof raw !== "string" || !raw.trim() || !HEADER_NAME_PATTERN.test(raw.trim())) {
|
|
49
|
+
return "forwardClientHeaders must use valid HTTP header names";
|
|
50
|
+
}
|
|
51
|
+
const name = raw.trim().toLowerCase();
|
|
52
|
+
if (BLOCKED_FORWARDED_CLIENT_HEADERS.has(name)) {
|
|
53
|
+
return `forwardClientHeaders must not include credential or transport-owned header "${raw.trim()}"`;
|
|
54
|
+
}
|
|
55
|
+
if (!ALLOWED_FORWARDED_CLIENT_HEADERS.has(name)) {
|
|
56
|
+
return "forwardClientHeaders must use the supported client metadata allowlist";
|
|
57
|
+
}
|
|
58
|
+
if (seen.has(name)) return `forwardClientHeaders must not repeat header "${raw.trim()}"`;
|
|
59
|
+
seen.add(name);
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
package/src/lib/proxy-env.ts
CHANGED
|
@@ -96,6 +96,9 @@ export function outboundProxyConfigured(
|
|
|
96
96
|
return OUTBOUND_PROXY_ENV_KEYS.some(key => proxyEnvPresent(key, env));
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
// Captured before config/proxy-env applies runtime changes; Bun retains startup proxy env.
|
|
100
|
+
export const startupOutboundProxyConfigured = outboundProxyConfigured();
|
|
101
|
+
|
|
99
102
|
/**
|
|
100
103
|
* The value when `raw` is a proxy URL Bun fetch can actually use, else null.
|
|
101
104
|
* Bun rejects unparseable values and non-http(s) schemes (UnsupportedProxyProtocol),
|
|
@@ -150,7 +150,8 @@ export function assertNotRealCodexHomeUnderTest(dir: string): void {
|
|
|
150
150
|
if (canonicalize(dir) !== PROTECTED_CODEX_HOME) return;
|
|
151
151
|
throw new Error(
|
|
152
152
|
`refusing to write the real Codex home (${PROTECTED_CODEX_HOME}) from a test process. `
|
|
153
|
-
+ "Point CODEX_HOME at a temp directory for this test before writing
|
|
153
|
+
+ "Point CODEX_HOME at a temp directory for this test before writing Codex files "
|
|
154
|
+
+ "(auth.json, the catalog, models_cache.json, the journal or config.toml).",
|
|
154
155
|
);
|
|
155
156
|
}
|
|
156
157
|
|
|
@@ -9,13 +9,13 @@ import type { OcxConfig } from "../types";
|
|
|
9
9
|
import type { AnthropicRouteDecision } from "./anthropic-model-routes";
|
|
10
10
|
import { recordAnthropicAccountRefusal, rotateAnthropicAccountOnRefusal, hasAnthropicFailoverQuorum, isAnthropicAccountPoolEnabled, pickAlternateAnthropicAccount } from "./anthropic-routing";
|
|
11
11
|
|
|
12
|
-
const responseCredentials = new WeakMap<Response, Pick<OAuthAccessSnapshot, "accountId" | "generation"
|
|
12
|
+
const responseCredentials = new WeakMap<Response, Pick<OAuthAccessSnapshot, "accountId" | "generation"> & { providerAccountUuid?: string; checkProviderUuid: boolean }>();
|
|
13
13
|
const retryStates = new WeakMap<object, { firstAccountId: string; sameAccount: boolean; detour: boolean }>();
|
|
14
14
|
const verdicts = new WeakMap<Response, Promise<boolean>>();
|
|
15
15
|
|
|
16
16
|
/** Called only when the outgoing headers prove ownership of the selected stored bearer. */
|
|
17
|
-
export function bindAnthropicRefusalCredential(response: Response, snapshot: OAuthAccessSnapshot): void {
|
|
18
|
-
responseCredentials.set(response, { accountId: snapshot.accountId, generation: snapshot.generation });
|
|
17
|
+
export function bindAnthropicRefusalCredential(response: Response, snapshot: OAuthAccessSnapshot, providerAccountUuid?: string): void {
|
|
18
|
+
responseCredentials.set(response, { accountId: snapshot.accountId, generation: snapshot.generation, providerAccountUuid, checkProviderUuid: arguments.length >= 3 });
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
async function isAccountRefusal(response: Response, signal?: AbortSignal): Promise<boolean> {
|
|
@@ -55,6 +55,10 @@ export async function rotateAnthropicAccountOnResponse(
|
|
|
55
55
|
requestKey?: object;
|
|
56
56
|
allow429Recovery?: boolean;
|
|
57
57
|
allowAccountRefusal?: boolean;
|
|
58
|
+
/** Applies to alternate ranking only; the shared same-account throttle remains allowed. */
|
|
59
|
+
excludedAccountIds?: ReadonlySet<string>;
|
|
60
|
+
/** Native dispatch re-reads operator policy after asynchronous classification/waits. */
|
|
61
|
+
currentDecision?: () => AnthropicRouteDecision | null;
|
|
58
62
|
},
|
|
59
63
|
): Promise<string | null> {
|
|
60
64
|
if (options.signal?.aborted || isNonReplayableResponse(response)) return null;
|
|
@@ -62,11 +66,13 @@ export async function rotateAnthropicAccountOnResponse(
|
|
|
62
66
|
const sent = responseCredentials.get(response);
|
|
63
67
|
const current = sent && getAccountCredentialWithStatus("anthropic", sent.accountId);
|
|
64
68
|
if (!sent || sent.accountId !== options.accountId || !current || current.needsReauth
|
|
65
|
-
|| credentialGeneration(current.credential) !== sent.generation
|
|
69
|
+
|| credentialGeneration(current.credential) !== sent.generation
|
|
70
|
+
|| sent.checkProviderUuid && current.credential.accountId !== sent.providerAccountUuid) return null;
|
|
71
|
+
options.currentDecision?.();
|
|
66
72
|
const kind = classifyAnthropic429(response.headers);
|
|
67
73
|
if (kind === "family-quota") {
|
|
68
74
|
if (anthropicModelFamily(options.model) !== "Fable" || !options.canRetry || options.allow429Recovery === false) return null;
|
|
69
|
-
return pickAlternateAnthropicAccount(options.config, sent.accountId, Date.now(), options.decision ?? null, options.model);
|
|
75
|
+
return pickAlternateAnthropicAccount(options.config, sent.accountId, Date.now(), (options.currentDecision ? options.currentDecision() : options.decision ?? null), options.model, options.excludedAccountIds);
|
|
70
76
|
}
|
|
71
77
|
if (kind !== "shared-quota") {
|
|
72
78
|
if (!isAnthropicAccountPoolEnabled(options.config) && !hasAnthropicFailoverQuorum() && !current.paused) return null;
|
|
@@ -87,16 +93,19 @@ export async function rotateAnthropicAccountOnResponse(
|
|
|
87
93
|
const live = getAccountCredentialWithStatus("anthropic", sent.accountId);
|
|
88
94
|
if (!live || live.paused || live.needsReauth || options.signal?.aborted
|
|
89
95
|
|| credentialGeneration(live.credential) !== sent.generation
|
|
96
|
+
|| sent.checkProviderUuid && live.credential.accountId !== sent.providerAccountUuid
|
|
90
97
|
|| anthropicRatePauseUntil(sent.accountId, Math.max(Date.now(), retryAt))) return null;
|
|
98
|
+
options.currentDecision?.();
|
|
91
99
|
return sent.accountId;
|
|
92
100
|
}
|
|
93
101
|
if (kind === "transient-rate" && !state.detour) {
|
|
94
102
|
state.detour = true;
|
|
95
|
-
return pickAlternateAnthropicAccount(options.config, sent.accountId, Date.now(), options.decision ?? null, options.model);
|
|
103
|
+
return pickAlternateAnthropicAccount(options.config, sent.accountId, Date.now(), (options.currentDecision ? options.currentDecision() : options.decision ?? null), options.model, options.excludedAccountIds);
|
|
96
104
|
}
|
|
97
105
|
return null;
|
|
98
106
|
}
|
|
99
107
|
if (options.allow429Recovery === false) {
|
|
108
|
+
options.currentDecision?.();
|
|
100
109
|
recordAnthropicAccountRefusal(options.config, options.accountId, 429, response.headers.get("retry-after"), Date.now(), response.headers);
|
|
101
110
|
return null;
|
|
102
111
|
}
|
|
@@ -112,8 +121,10 @@ export async function rotateAnthropicAccountOnResponse(
|
|
|
112
121
|
if (!await verdict || options.signal?.aborted) return null;
|
|
113
122
|
// A late refusal must not cool a new credential stored while its body was being read.
|
|
114
123
|
const current = getAccountCredentialWithStatus("anthropic", sent.accountId);
|
|
115
|
-
if (!current || current.paused || current.needsReauth || credentialGeneration(current.credential) !== sent.generation
|
|
124
|
+
if (!current || current.paused || current.needsReauth || credentialGeneration(current.credential) !== sent.generation
|
|
125
|
+
|| sent.checkProviderUuid && current.credential.accountId !== sent.providerAccountUuid) return null;
|
|
116
126
|
}
|
|
127
|
+
const decision = options.currentDecision ? options.currentDecision() : options.decision;
|
|
117
128
|
const status = response.status === 429 ? 429 : 403;
|
|
118
129
|
if (!options.canRetry) {
|
|
119
130
|
recordAnthropicAccountRefusal(options.config, options.accountId, status,
|
|
@@ -121,5 +132,5 @@ export async function rotateAnthropicAccountOnResponse(
|
|
|
121
132
|
return null;
|
|
122
133
|
}
|
|
123
134
|
return rotateAnthropicAccountOnRefusal(options.config, options.accountId, status,
|
|
124
|
-
response.headers.get("retry-after"), options.sessionKey, Date.now(), response.headers, options.
|
|
135
|
+
response.headers.get("retry-after"), options.sessionKey, Date.now(), response.headers, decision, options.model, options.excludedAccountIds);
|
|
125
136
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { detectClaudeCodeToken, hasClaudeCredentialContinuity } from "./local-token-detect";
|
|
2
|
+
import { normalizeAnthropicIdentity, resolveAnthropicAccountIdentity, type AnthropicIdentityResolver } from "./anthropic-identity";
|
|
3
|
+
import { credentialGeneration, type AuthStore } from "./store";
|
|
4
|
+
import { REFRESH_SKEW_MS } from "./refresh-policy";
|
|
5
|
+
import type { OAuthCredentials, ProviderAccountSet } from "./types";
|
|
6
|
+
|
|
7
|
+
export type ClaudeCredentialObservation =
|
|
8
|
+
| { kind: "absent" }
|
|
9
|
+
| { kind: "unresolved" | "different"; diskGeneration: string }
|
|
10
|
+
| { kind: "adopt"; credential: OAuthCredentials; diskGeneration: string };
|
|
11
|
+
|
|
12
|
+
export async function newerClaudeCredential(
|
|
13
|
+
stored: OAuthCredentials, now: number, signal?: AbortSignal,
|
|
14
|
+
resolveIdentity: AnthropicIdentityResolver = resolveAnthropicAccountIdentity,
|
|
15
|
+
): Promise<ClaudeCredentialObservation> {
|
|
16
|
+
if (stored.source !== "local-cli") return { kind: "absent" };
|
|
17
|
+
const disk = detectClaudeCodeToken();
|
|
18
|
+
if (!disk || !Number.isFinite(disk.expires) || disk.expires <= now + REFRESH_SKEW_MS
|
|
19
|
+
|| credentialGeneration(disk) === credentialGeneration(stored)) return { kind: "absent" };
|
|
20
|
+
const diskGeneration = credentialGeneration(disk);
|
|
21
|
+
const before = normalizeAnthropicIdentity(stored.anthropicIdentity, stored.access);
|
|
22
|
+
if (hasClaudeCredentialContinuity(stored, disk)) {
|
|
23
|
+
return { kind: "adopt", diskGeneration: credentialGeneration(disk), credential: {
|
|
24
|
+
...stored, ...disk, anthropicIdentity: disk.access === stored.access ? before : undefined,
|
|
25
|
+
} };
|
|
26
|
+
}
|
|
27
|
+
// Independent observations share a deadline through the caller signal; neither extends the other.
|
|
28
|
+
const [oldProof, newProof] = await Promise.all([
|
|
29
|
+
before ?? resolveIdentity(stored.access, signal), resolveIdentity(disk.access, signal),
|
|
30
|
+
]);
|
|
31
|
+
const oldIdentity = normalizeAnthropicIdentity(oldProof, stored.access);
|
|
32
|
+
const newIdentity = normalizeAnthropicIdentity(newProof, disk.access);
|
|
33
|
+
if (!oldIdentity || !newIdentity) return { kind: "unresolved", diskGeneration };
|
|
34
|
+
if (oldIdentity.accountUuid !== newIdentity.accountUuid) return { kind: "different", diskGeneration };
|
|
35
|
+
return { kind: "adopt", diskGeneration: credentialGeneration(disk), credential: {
|
|
36
|
+
...stored, ...disk, accountId: newIdentity.accountUuid, anthropicIdentity: newIdentity,
|
|
37
|
+
} };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Capture primitives before network awaits; check them again inside the serialized writer. */
|
|
41
|
+
export function captureAnthropicCredentialOwner(set: ProviderAccountSet, accountId: string) {
|
|
42
|
+
const row = set.accounts.find(account => account.id === accountId)!;
|
|
43
|
+
const loginId = row.loginId;
|
|
44
|
+
const generation = credentialGeneration(row.credential);
|
|
45
|
+
const metadata = identityMetadata(row.credential);
|
|
46
|
+
const needsReauth = row.needsReauth;
|
|
47
|
+
const selection = { id: set.activeAccountId, revision: set.selectionRevision };
|
|
48
|
+
return (store: AuthStore, diskGeneration?: string, allowPaused = false): boolean => {
|
|
49
|
+
const current = store["anthropic"];
|
|
50
|
+
const account = current?.accounts.find(candidate => candidate.id === accountId);
|
|
51
|
+
if (!account || (!allowPaused && account.paused) || account.loginId !== loginId
|
|
52
|
+
|| account.needsReauth !== needsReauth || credentialGeneration(account.credential) !== generation
|
|
53
|
+
|| identityMetadata(account.credential) !== metadata) return false;
|
|
54
|
+
if (diskGeneration !== undefined) {
|
|
55
|
+
if (current?.activeAccountId !== selection.id || current?.selectionRevision !== selection.revision) return false;
|
|
56
|
+
const disk = detectClaudeCodeToken();
|
|
57
|
+
if (!disk || disk.expires <= Date.now() + REFRESH_SKEW_MS || credentialGeneration(disk) !== diskGeneration) return false;
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function identityMetadata(credential: OAuthCredentials): string {
|
|
64
|
+
return JSON.stringify([credential.source, credential.accountId, credential.email, credential.anthropicIdentity]);
|
|
65
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/** Private account proof from an authenticated Anthropic response, bound to its exact bearer. */
|
|
2
|
+
import { hasClaudeCredentialContinuity } from "./local-token-detect";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
5
|
+
import { CLAUDE_CLI_USER_AGENT } from "../providers/claude-cli-identity";
|
|
6
|
+
import type { OAuthCredentials } from "./types";
|
|
7
|
+
|
|
8
|
+
const PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
|
|
9
|
+
const PROFILE_TIMEOUT_MS = 10_000;
|
|
10
|
+
const PROFILE_MAX_BYTES = 65_536;
|
|
11
|
+
export type AnthropicIdentity = NonNullable<OAuthCredentials["anthropicIdentity"]>;
|
|
12
|
+
export type AnthropicIdentityResolver = (access: string, signal?: AbortSignal) => Promise<AnthropicIdentity | undefined>;
|
|
13
|
+
|
|
14
|
+
export function bindAnthropicIdentity(access: string, accountUuid: unknown): AnthropicIdentity | undefined {
|
|
15
|
+
if (!access || typeof accountUuid !== "string" || !accountUuid.length || accountUuid.length > 128
|
|
16
|
+
|| accountUuid !== accountUuid.trim() || /[\x00-\x1f\x7f]/.test(accountUuid)) return undefined;
|
|
17
|
+
return { v: 1, accountUuid, bearerSha256: createHash("sha256").update(access).digest("hex") };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function normalizeAnthropicIdentity(value: unknown, access: string): AnthropicIdentity | undefined {
|
|
21
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
22
|
+
const proof = value as Partial<AnthropicIdentity>;
|
|
23
|
+
const bound = bindAnthropicIdentity(access, proof.accountUuid);
|
|
24
|
+
return proof.v === 1 && bound && proof.bearerSha256 === bound.bearerSha256 ? bound : undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Fixed destination, no redirects, bounded body and deadline; failures never include response data. */
|
|
28
|
+
export const resolveAnthropicAccountIdentity: AnthropicIdentityResolver = async (access, signal) => {
|
|
29
|
+
if (!access || signal?.aborted) return undefined;
|
|
30
|
+
const deadline = AbortSignal.timeout(PROFILE_TIMEOUT_MS);
|
|
31
|
+
const combined = signal ? AbortSignal.any([signal, deadline]) : deadline;
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetch(PROFILE_URL, {
|
|
34
|
+
method: "GET", redirect: "error", signal: combined,
|
|
35
|
+
headers: { Authorization: `Bearer ${access}`, Accept: "application/json",
|
|
36
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
37
|
+
"Cache-Control": "no-cache", "User-Agent": CLAUDE_CLI_USER_AGENT },
|
|
38
|
+
});
|
|
39
|
+
if (!response.ok || response.redirected) {
|
|
40
|
+
void response.body?.cancel().catch(() => {});
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const body = await readBoundedResponseBody(response, {
|
|
44
|
+
signal: combined, maxBytes: PROFILE_MAX_BYTES, fatalUtf8: true,
|
|
45
|
+
totalTimeoutMs: PROFILE_TIMEOUT_MS, inactivityTimeoutMs: PROFILE_TIMEOUT_MS,
|
|
46
|
+
});
|
|
47
|
+
if (body.truncated || body.oversized || body.timedOut || combined.aborted) return undefined;
|
|
48
|
+
const data: unknown = JSON.parse(body.text);
|
|
49
|
+
if (!data || typeof data !== "object" || !("account" in data)) return undefined;
|
|
50
|
+
const account = data.account;
|
|
51
|
+
return account && typeof account === "object" && "uuid" in account
|
|
52
|
+
? bindAnthropicIdentity(access, account.uuid) : undefined;
|
|
53
|
+
} catch {
|
|
54
|
+
// Unavailable identity is an explicit unresolved result, never an ownership guess.
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** A changed access bearer cannot inherit the previous bearer's proof. */
|
|
60
|
+
export function mergeAnthropicIdentity(fresh: OAuthCredentials, previous: OAuthCredentials): Pick<OAuthCredentials, "anthropicIdentity"> {
|
|
61
|
+
const before = normalizeAnthropicIdentity(previous.anthropicIdentity, previous.access);
|
|
62
|
+
const after = normalizeAnthropicIdentity(fresh.anthropicIdentity, fresh.access);
|
|
63
|
+
if (before && after && before.accountUuid !== after.accountUuid) {
|
|
64
|
+
throw new Error("Anthropic credential identity changed; explicit account login required");
|
|
65
|
+
}
|
|
66
|
+
return { anthropicIdentity: after ?? (fresh.access === previous.access ? before : undefined) };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Explicit imports enrich only a proven slot; labels and paths never establish ownership. */
|
|
70
|
+
export function matchesAnthropicImport(stored: OAuthCredentials, imported: OAuthCredentials): boolean {
|
|
71
|
+
const before = normalizeAnthropicIdentity(stored.anthropicIdentity, stored.access);
|
|
72
|
+
const after = normalizeAnthropicIdentity(imported.anthropicIdentity, imported.access);
|
|
73
|
+
if (before && after) return before.accountUuid === after.accountUuid;
|
|
74
|
+
return hasClaudeCredentialContinuity(stored, imported);
|
|
75
|
+
}
|
|
@@ -18,6 +18,7 @@ import { anthropicFamilyRejected, anthropicFamilyRetryAt, anthropicModelExhauste
|
|
|
18
18
|
* Token refresh failures retain the existing store needsReauth policy.
|
|
19
19
|
*/
|
|
20
20
|
import { anthropicRatePauseUntil, clearAnthropicRatePauses, classifyAnthropic429, anthropicRetryAfterMs, pauseAnthropicRateAdmission } from "./anthropic-rate-limit-policy";
|
|
21
|
+
import { REFRESH_SKEW_MS } from "./refresh-policy";
|
|
21
22
|
import { createHash } from "node:crypto";
|
|
22
23
|
import { captureOAuthAccountSelection, commitOAuthAccountSelection, credentialGeneration, getAccountSet, getAccountCredential, getAccountCredentialWithStatus } from "./store";
|
|
23
24
|
import type { OAuthAccessSnapshot } from "./index";
|
|
@@ -110,6 +111,9 @@ type OAuthAccountSelection = NonNullable<ReturnType<typeof captureOAuthAccountSe
|
|
|
110
111
|
// Undefined means this runtime has not admitted a selection yet; null means consumed.
|
|
111
112
|
// The startup baseline comes from the authoritative store, never a second persisted pin.
|
|
112
113
|
let manualPreference: OAuthAccountSelection | null | undefined;
|
|
114
|
+
let manualSelectionGeneration = 0;
|
|
115
|
+
/** Automatic pointer moves preserve affinity; manual selection revokes older bindings. */
|
|
116
|
+
export function captureAnthropicManualSelectionGeneration(): number { return manualSelectionGeneration; }
|
|
113
117
|
|
|
114
118
|
function normalizeAffinityComponent(value: string | null | undefined): string {
|
|
115
119
|
const normalized = value?.trim() ?? "";
|
|
@@ -284,6 +288,7 @@ export function clearAnthropicAccountPoolState(): void {
|
|
|
284
288
|
clearAnthropicCooldownGenerations();
|
|
285
289
|
sessionAffinity.clear();
|
|
286
290
|
manualPreference = undefined;
|
|
291
|
+
manualSelectionGeneration++;
|
|
287
292
|
quorumCache = null;
|
|
288
293
|
}
|
|
289
294
|
|
|
@@ -353,7 +358,6 @@ function usageScore(config: OcxConfig, accountId: string, model?: string): numbe
|
|
|
353
358
|
}
|
|
354
359
|
}
|
|
355
360
|
|
|
356
|
-
const TOKEN_SKEW_MS = 60_000;
|
|
357
361
|
|
|
358
362
|
/** Background `local-cli` slots with expired access are not pool-eligible (identity adoption risk). */
|
|
359
363
|
function isPoolCredentialUsable(accountId: string, now: number): boolean {
|
|
@@ -361,7 +365,7 @@ function isPoolCredentialUsable(accountId: string, now: number): boolean {
|
|
|
361
365
|
if (!cred) return false;
|
|
362
366
|
if (cred.source !== "local-cli") return true;
|
|
363
367
|
if (canRefreshAnthropicPoolAccount(accountId)) return true;
|
|
364
|
-
return cred.expires > now +
|
|
368
|
+
return cred.expires > now + REFRESH_SKEW_MS;
|
|
365
369
|
}
|
|
366
370
|
|
|
367
371
|
export function getEligibleAnthropicAccounts(now = Date.now(), model?: string): string[] {
|
|
@@ -508,9 +512,9 @@ function compareScoredAccounts(a: ScoredAccount, b: ScoredAccount): number {
|
|
|
508
512
|
return a.score - b.score || a.fiveHourTieBreak - b.fiveHourTieBreak;
|
|
509
513
|
}
|
|
510
514
|
|
|
511
|
-
function pickLowestUsage(config: OcxConfig, excludeId: string | undefined, now: number, decision: AnthropicRouteDecision | null = null, model?: string): string | null {
|
|
515
|
+
function pickLowestUsage(config: OcxConfig, excludeId: string | undefined, now: number, decision: AnthropicRouteDecision | null = null, model?: string, excludedAccountIds?: ReadonlySet<string>): string | null {
|
|
512
516
|
const window = anthropicQuotaWindow(anthropicAccountPoolConfig(config));
|
|
513
|
-
const unfiltered = routeCandidates(getEligibleAnthropicAccounts(now, model), decision).filter(id => id !== excludeId);
|
|
517
|
+
const unfiltered = routeCandidates(getEligibleAnthropicAccounts(now, model), decision).filter(id => id !== excludeId && !excludedAccountIds?.has(id));
|
|
514
518
|
const available = window === "weekly" ? unfiltered.filter(id => !exhausted5h(id)
|
|
515
519
|
|| isAnthropicAccountPoolEnabled(config) && anthropicAccountAutoSwitchThreshold(config, id) === 0) : unfiltered;
|
|
516
520
|
const modelAvailable = model ? available.filter(id => !anthropicModelExhausted(id, model)) : available;
|
|
@@ -589,16 +593,17 @@ export function pickAlternateAnthropicAccount(
|
|
|
589
593
|
now: number,
|
|
590
594
|
decision: AnthropicRouteDecision | null,
|
|
591
595
|
model?: string,
|
|
596
|
+
excludedAccountIds?: ReadonlySet<string>,
|
|
592
597
|
): string | null {
|
|
593
598
|
const strategy = anthropicPoolStrategy(config);
|
|
594
|
-
const eligible = routeCandidates(getEligibleAnthropicAccounts(now, model), decision).filter(id => id !== excludeId);
|
|
599
|
+
const eligible = routeCandidates(getEligibleAnthropicAccounts(now, model), decision).filter(id => id !== excludeId && !excludedAccountIds?.has(id));
|
|
595
600
|
if (strategy === "round-robin") {
|
|
596
601
|
return peekRoundRobinAccount(POOL_KEY_ANTHROPIC, eligible, stickyLimitForPool(config));
|
|
597
602
|
}
|
|
598
603
|
if (strategy === "fill-first") {
|
|
599
604
|
return pickNextFillFirstAnthropicAccount(config, excludeId, eligible, decision, model);
|
|
600
605
|
}
|
|
601
|
-
return pickLowestUsage(config, excludeId, now, decision, model);
|
|
606
|
+
return pickLowestUsage(config, excludeId, now, decision, model, excludedAccountIds);
|
|
602
607
|
}
|
|
603
608
|
|
|
604
609
|
function pruneExpiredAffinity(now: number): void {
|
|
@@ -918,6 +923,7 @@ export function rotateAnthropicAccountOnRefusal(
|
|
|
918
923
|
rateLimitHeaders?: AnthropicRateLimitHeaders | null,
|
|
919
924
|
decision: AnthropicRouteDecision | null = null,
|
|
920
925
|
model?: string,
|
|
926
|
+
excludedAccountIds?: ReadonlySet<string>,
|
|
921
927
|
): string | null {
|
|
922
928
|
if (!recordAnthropicAccountRefusal(config, failedAccountId, status, retryAfterHeader, now, rateLimitHeaders)) return null;
|
|
923
929
|
|
|
@@ -926,8 +932,8 @@ export function rotateAnthropicAccountOnRefusal(
|
|
|
926
932
|
// because those dormant values remain in config. The quota picker is the neutral
|
|
927
933
|
// recovery policy already used by the default strategy.
|
|
928
934
|
const next = isAnthropicAccountPoolEnabled(config)
|
|
929
|
-
? pickAlternateAnthropicAccount(config, failedAccountId, now, decision, model)
|
|
930
|
-
: pickLowestUsage(config, failedAccountId, now, null, model);
|
|
935
|
+
? pickAlternateAnthropicAccount(config, failedAccountId, now, decision, model, excludedAccountIds)
|
|
936
|
+
: pickLowestUsage(config, failedAccountId, now, null, model, excludedAccountIds);
|
|
931
937
|
if (!next) {
|
|
932
938
|
console.warn(`[anthropic-pool] ${decision ? `route:#${decision.position} ` : ""}no eligible replacement; returning ${status}`);
|
|
933
939
|
return null;
|
|
@@ -1070,6 +1076,7 @@ export function commitAnthropicSelectionRouting(
|
|
|
1070
1076
|
*/
|
|
1071
1077
|
export function resetAnthropicRoutingForManualSelection(accountId: string): void {
|
|
1072
1078
|
sessionAffinity.clear();
|
|
1079
|
+
manualSelectionGeneration++;
|
|
1073
1080
|
manualPreference = captureOAuthAccountSelection(PROVIDER);
|
|
1074
1081
|
seedPoolRotationAccount(POOL_KEY_ANTHROPIC, accountId);
|
|
1075
1082
|
// A manual account selection is an operator statement about the roster; do not answer the
|
|
@@ -1093,7 +1100,7 @@ export async function getAnthropicPoolAccessToken(accountId: string): Promise<st
|
|
|
1093
1100
|
const { OAuthLoginRequiredError } = await import("./index");
|
|
1094
1101
|
throw new OAuthLoginRequiredError(PROVIDER);
|
|
1095
1102
|
}
|
|
1096
|
-
if (stored.expires > Date.now() +
|
|
1103
|
+
if (stored.expires > Date.now() + REFRESH_SKEW_MS) return stored.access;
|
|
1097
1104
|
if (!canRefreshAnthropicPoolAccount(accountId)) {
|
|
1098
1105
|
throw new Error("background local-cli token expired; refuse CLI-adopting refresh for pool");
|
|
1099
1106
|
}
|
|
@@ -1172,12 +1179,10 @@ export function anthropicSessionKeyFromParts(input: {
|
|
|
1172
1179
|
/** When true, prompt_cache_key is a shared Desktop cohort — ignore it for affinity. */
|
|
1173
1180
|
promptCacheKeyIsSharedCohort?: boolean;
|
|
1174
1181
|
}): string | null {
|
|
1175
|
-
const preferred = (
|
|
1176
|
-
input.
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
?? ""
|
|
1180
|
-
).trim();
|
|
1182
|
+
const preferred = input.clientThreadId?.trim()
|
|
1183
|
+
|| input.sessionIdHeader?.trim()
|
|
1184
|
+
|| input.threadIdHeader?.trim()
|
|
1185
|
+
|| "";
|
|
1181
1186
|
if (preferred) {
|
|
1182
1187
|
return preferred.length <= 128 ? preferred : createHash("sha256").update(preferred).digest("hex");
|
|
1183
1188
|
}
|
package/src/oauth/anthropic.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/** Anthropic OAuth flow (Claude Pro/Max). Ported from jawcode oauth/anthropic.ts. */
|
|
2
2
|
import { OAuthCallbackFlow } from "./callback-server";
|
|
3
|
+
import { bindAnthropicIdentity, resolveAnthropicAccountIdentity } from "./anthropic-identity";
|
|
3
4
|
import { generatePKCE } from "./pkce";
|
|
4
5
|
import type { LocalTokenImportMode, OAuthController, OAuthCredentials } from "./types";
|
|
6
|
+
import { outboundProxyConfigured, startupOutboundProxyConfigured } from "../lib/proxy-env";
|
|
5
7
|
|
|
6
8
|
const CLIENT_ID = atob("OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl");
|
|
7
9
|
const AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
|
|
@@ -39,6 +41,7 @@ export class AnthropicTokenError extends Error {
|
|
|
39
41
|
message: string,
|
|
40
42
|
readonly httpStatus: number | undefined,
|
|
41
43
|
readonly oauthError: string | undefined,
|
|
44
|
+
readonly requestNotSent = false,
|
|
42
45
|
) {
|
|
43
46
|
super(message);
|
|
44
47
|
this.name = "AnthropicTokenError";
|
|
@@ -46,12 +49,35 @@ export class AnthropicTokenError extends Error {
|
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
async function postJson(url: string, body: Record<string, string | number>): Promise<string> {
|
|
49
|
-
const
|
|
52
|
+
const direct = !startupOutboundProxyConfigured && !outboundProxyConfigured();
|
|
53
|
+
const init: RequestInit & { protocol: "http1.1" } = {
|
|
50
54
|
method: "POST",
|
|
51
55
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
52
56
|
body: JSON.stringify(body),
|
|
53
57
|
signal: AbortSignal.timeout(30_000),
|
|
54
|
-
|
|
58
|
+
// Pinned Bun 1.4.0: one HTTP/1.1 attempt, no keep-alive reuse or redirect following.
|
|
59
|
+
// Only without an outbound proxy can token-host DNS failure prove no request was sent.
|
|
60
|
+
redirect: "manual",
|
|
61
|
+
keepalive: false,
|
|
62
|
+
protocol: "http1.1",
|
|
63
|
+
};
|
|
64
|
+
let response: Response;
|
|
65
|
+
try {
|
|
66
|
+
response = await fetch(url, init);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (direct && typeof error === "object" && error !== null
|
|
69
|
+
&& "code" in error && error.code === "ENOTFOUND"
|
|
70
|
+
&& "syscall" in error && error.syscall === "getaddrinfo"
|
|
71
|
+
&& "hostname" in error && error.hostname === new URL(url).hostname) {
|
|
72
|
+
throw new AnthropicTokenError("Anthropic OAuth DNS lookup failed before connection", undefined, undefined, true);
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
if (response.status >= 300 && response.status < 400) {
|
|
77
|
+
// A redirect can follow a completed POST and token rotation; it is not a rejection.
|
|
78
|
+
await response.body?.cancel().catch(() => {});
|
|
79
|
+
throw new AnthropicTokenError(`Anthropic OAuth redirect HTTP ${response.status}: outcome unknown`, undefined, undefined);
|
|
80
|
+
}
|
|
55
81
|
const responseBody = await response.text();
|
|
56
82
|
if (!response.ok) {
|
|
57
83
|
let oauthError: string | undefined;
|
|
@@ -96,6 +122,7 @@ function credsFrom(data: AnthropicTokenResponse, refreshFallback?: string): OAut
|
|
|
96
122
|
refresh: data.refresh_token || refreshFallback || "",
|
|
97
123
|
access: data.access_token,
|
|
98
124
|
expires,
|
|
125
|
+
anthropicIdentity: bindAnthropicIdentity(data.access_token, accountUuid),
|
|
99
126
|
accountId: typeof accountUuid === "string" && accountUuid.length > 0 ? accountUuid : undefined,
|
|
100
127
|
email: typeof email === "string" && email.length > 0 ? email : undefined,
|
|
101
128
|
};
|
|
@@ -159,7 +186,11 @@ export async function loginAnthropic(
|
|
|
159
186
|
const local = detectClaudeCodeToken();
|
|
160
187
|
if (local) {
|
|
161
188
|
ctrl.onProgress?.("Found Claude Code token, importing automatically");
|
|
162
|
-
if (local.expires >= Date.now() + 60_000)
|
|
189
|
+
if (local.expires >= Date.now() + 60_000) {
|
|
190
|
+
const identity = await resolveAnthropicAccountIdentity(local.access, ctrl.signal);
|
|
191
|
+
ctrl.signal?.throwIfAborted();
|
|
192
|
+
return { ...local, ...(identity ? { accountId: identity.accountUuid, anthropicIdentity: identity } : {}) };
|
|
193
|
+
}
|
|
163
194
|
try {
|
|
164
195
|
return { ...(await refreshAnthropicToken(local.refresh)), source: "local-cli" };
|
|
165
196
|
} catch (error) {
|
|
@@ -734,3 +734,24 @@ export function clearGenericFailoverHealth(providerName?: string): void {
|
|
|
734
734
|
if (key.startsWith(`${providerName}\u0000`)) health.delete(key);
|
|
735
735
|
}
|
|
736
736
|
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Retire one account's superseded auth evidence after an explicit (re-)login. An
|
|
740
|
+
* auth cooldown recorded against the old grant must not hold out the fresh
|
|
741
|
+
* credential, so it leaves with the login rather than lingering until its
|
|
742
|
+
* deadline. Rate, quota, and suspension evidence is preserved: a re-login says
|
|
743
|
+
* nothing about those verdicts, and dropping them would make a throttled account
|
|
744
|
+
* eligible early. Entries bound to the still-current generation stay as well.
|
|
745
|
+
*/
|
|
746
|
+
export function clearGenericFailoverHealthForAccount(providerName: string, accountId: string): void {
|
|
747
|
+
const live = getAccountSet(providerName)?.accounts.find(row => row.id === accountId);
|
|
748
|
+
const current = live ? credentialGeneration(live.credential) : undefined;
|
|
749
|
+
const bare = `${providerName}\u0000${accountId}`;
|
|
750
|
+
for (const key of [...health.keys()]) {
|
|
751
|
+
if (key !== bare && !key.startsWith(`${bare}\u0000`)) continue;
|
|
752
|
+
const entry = health.get(key);
|
|
753
|
+
if (!entry || entry.cooldownSource !== "auth") continue;
|
|
754
|
+
if (current !== undefined && entry.identity === current) continue;
|
|
755
|
+
health.delete(key);
|
|
756
|
+
}
|
|
757
|
+
}
|