@bitkyc08/opencodex 2.84.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/README.md +0 -4
- package/bin/ocx.mjs +61 -3
- package/gui/dist/assets/App-ZfYbLHkB.js +58 -0
- package/gui/dist/assets/App-wT3mXqFz.css +1 -0
- package/gui/dist/assets/{Tray-C9TGtwad.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 -2
- package/src/adapters/anthropic/beta-allowlist.ts +91 -11
- package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
- package/src/adapters/anthropic/client-identity.ts +1 -1
- package/src/adapters/anthropic/passthrough.ts +16 -3
- package/src/adapters/anthropic-progress.ts +84 -0
- package/src/adapters/anthropic.ts +108 -21
- package/src/adapters/azure.ts +19 -1
- 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/client-fingerprint.ts +24 -0
- package/src/adapters/coding-agent/protocol.ts +1 -1
- package/src/adapters/coding-agent/turn.ts +40 -11
- package/src/adapters/cursor/arg-codec.ts +17 -5
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/cursor/freeform-args.ts +75 -0
- package/src/adapters/cursor/protobuf-events.ts +36 -23
- package/src/adapters/dsh-account.ts +97 -0
- package/src/adapters/google-antigravity-wire.ts +22 -1
- package/src/adapters/google.ts +37 -10
- 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-chat/response-events.ts +59 -1
- package/src/adapters/openai-chat.ts +8 -5
- 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 +44 -3
- 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 +223 -0
- package/src/advisor/context.ts +238 -0
- package/src/advisor/disclosure.ts +20 -0
- package/src/advisor/evidence-tool.ts +21 -0
- package/src/advisor/evidence.ts +117 -0
- package/src/advisor/runtime.ts +353 -0
- package/src/advisor/settings.ts +144 -0
- package/src/advisor/state.ts +369 -0
- package/src/advisor/synthetic-tool.ts +33 -0
- package/src/bridge/errors.ts +24 -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/chat/inbound.ts +30 -6
- package/src/chat/outbound.ts +5 -0
- package/src/claude/desktop-3p-library.ts +72 -7
- package/src/claude/desktop-3p.ts +75 -19
- package/src/claude/desktop-first-party.ts +1 -1
- package/src/claude/desktop-picker-platform.ts +4 -0
- package/src/claude/desktop-picker-profile.ts +11 -4
- package/src/claude/desktop-picker.ts +15 -8
- package/src/claude/desktop-profile.ts +10 -1
- package/src/claude/desktop-remote-store-io.ts +5 -3
- package/src/claude/desktop-system-proxy.ts +50 -4
- package/src/claude/inbound-content-options.ts +11 -0
- package/src/claude/inbound.ts +154 -50
- package/src/claude/intercept/local-ca.ts +8 -1
- 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/model-default-settings.ts +154 -0
- package/src/claude/model-info.ts +7 -7
- package/src/claude/outbound.ts +51 -19
- 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 +165 -0
- package/src/cli/auxiliary-listener-preflight.ts +37 -0
- package/src/cli/capabilities-accounts.ts +20 -1
- package/src/cli/capabilities-agents-routing.ts +23 -2
- package/src/cli/capabilities-base.ts +39 -11
- package/src/cli/capabilities-observe-system.ts +63 -3
- package/src/cli/capabilities-provider-models.ts +11 -0
- package/src/cli/chatgpt-command.ts +35 -14
- package/src/cli/claude.ts +46 -6
- 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 +30 -5
- package/src/cli/doctor.ts +15 -1
- package/src/cli/ensure-desktop-cold-start.ts +382 -0
- package/src/cli/export-command.ts +1 -1
- package/src/cli/help.ts +4 -2
- package/src/cli/index.ts +14 -5
- package/src/cli/integration-plan-dto.ts +3 -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 +41 -9
- package/src/cli/restart-scope.ts +3 -1
- 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 +3 -1
- package/src/clients/config-export/model-metadata.ts +3 -2
- package/src/clients/config-export/qoder.ts +93 -0
- package/src/clients/config-export.ts +35 -48
- 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/pool-mode-gate.ts +5 -2
- 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 +81 -7
- package/src/codex/autostart-health.ts +4 -0
- package/src/codex/catalog/auto-review.ts +20 -3
- package/src/codex/catalog/build-entries.ts +19 -2
- package/src/codex/catalog/combo-member.ts +14 -5
- package/src/codex/catalog/configured-tiers.ts +50 -0
- package/src/codex/catalog/effort.ts +4 -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 +42 -19
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/native-ultrafast.ts +106 -0
- package/src/codex/catalog/parsing.ts +35 -86
- package/src/codex/catalog/provider-models.ts +40 -34
- package/src/codex/catalog/retained-sync.ts +2 -0
- package/src/codex/catalog/routed-gather.ts +33 -16
- package/src/codex/catalog/vendor-context.ts +82 -0
- 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 +63 -4
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desktop-app/picker-freshness.ts +96 -0
- package/src/codex/desktop-app/types.ts +2 -0
- package/src/codex/desktop-app/windows.ts +29 -16
- package/src/codex/desktop-app-restart.ts +25 -2
- package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
- package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
- package/src/codex/desktop-compatibility/connection-store.ts +87 -0
- package/src/codex/desktop-compatibility/installed-build.ts +73 -0
- package/src/codex/desktop-compatibility/json-body.ts +29 -0
- package/src/codex/desktop-compatibility/native-identity.ts +62 -0
- package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
- package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
- package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
- package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
- package/src/codex/desktop-compatibility/runtime.ts +182 -0
- package/src/codex/desktop-compatibility/service.ts +30 -0
- package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
- package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
- package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
- package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
- package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
- package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
- package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
- package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
- package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
- package/src/codex/desktop-compatibility/windows-key-protection.ts +165 -0
- package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
- package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
- package/src/codex/features.ts +85 -45
- package/src/codex/history-provider.ts +1 -1
- package/src/codex/inject/config-write-section.ts +4 -1
- package/src/codex/inject/history-failure.ts +27 -0
- package/src/codex/inject/remove.ts +18 -6
- 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/journal.ts +16 -1
- package/src/codex/main-account-hard-lock.ts +30 -1
- package/src/codex/main-account.ts +99 -15
- package/src/codex/native-main-refresh-mode.ts +217 -0
- 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 +2 -2
- package/src/codex/routing/pool-recovery.ts +79 -0
- package/src/codex/routing/selection.ts +4 -3
- package/src/codex/routing/thread-affinity.ts +1 -1
- package/src/codex/shim-diagnostics.ts +6 -0
- package/src/codex/shim-overlay.ts +7 -2
- 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 +35 -7
- package/src/config/live-reconcile.ts +51 -2
- package/src/config/load-degrade.ts +23 -1
- 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/paths.ts +10 -3
- package/src/config/provider-validation.ts +45 -2
- package/src/config/schema/config-schema.ts +39 -0
- package/src/config/schema/desktop-compatibility.ts +9 -0
- package/src/config/schema/leaf-validators.ts +39 -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 +962 -350
- package/src/generated/model-metadata.ts +1 -1
- 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/config-io.ts +20 -2
- package/src/integrations/journal.ts +15 -1
- package/src/integrations/mutation-plan.ts +2 -0
- package/src/integrations/registry.ts +15 -0
- package/src/integrations/store.ts +3 -0
- package/src/integrations/writer.ts +10 -1
- package/src/lib/advisor-activation.ts +16 -0
- package/src/lib/background-work.ts +65 -0
- package/src/lib/bounded-file-read.ts +58 -0
- package/src/lib/bun-path-runtime.d.mts +10 -1
- package/src/lib/bun-path-runtime.mjs +62 -3
- package/src/lib/codex-restart-contract.ts +15 -0
- package/src/lib/desktop-proxy-route.ts +30 -0
- package/src/lib/desktop-upstream-tunnel.ts +72 -0
- package/src/lib/empty-tool-catalog.ts +62 -0
- package/src/lib/errors.ts +26 -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/local-internal-call-capability.ts +55 -0
- package/src/lib/opaque-proxy-tunnel.ts +217 -0
- package/src/lib/package-tree-integrity.ts +2 -2
- package/src/lib/redact.ts +2 -2
- package/src/lib/socks5-fetch.ts +48 -71
- package/src/lib/socks5-handshake.ts +112 -0
- package/src/lib/translator-budget.ts +7 -1
- 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 +62 -12
- package/src/oauth/generic-session-affinity.ts +125 -0
- package/src/oauth/index.ts +188 -111
- package/src/oauth/kiro-device-login.ts +2 -1
- package/src/oauth/kiro.ts +7 -1
- 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/codecs/chat-ir.ts +467 -0
- package/src/protocols/codecs/chat.ts +10 -1
- package/src/protocols/codecs/messages-ir.ts +500 -0
- package/src/protocols/encoders/adapter-events.ts +5 -4
- package/src/protocols/encoders/messages.ts +9 -6
- package/src/protocols/features.ts +10 -2
- package/src/protocols/path.ts +11 -1
- package/src/protocols/plan-snapshot.ts +39 -16
- package/src/protocols/plan.ts +9 -1
- package/src/protocols/provider-summary.ts +2 -1
- package/src/protocols/settings.ts +8 -0
- package/src/providers/antigravity-account-roster.ts +102 -0
- package/src/providers/antigravity-models.ts +85 -7
- 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 +5 -0
- package/src/providers/model-rename-migration.ts +7 -5
- 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 +44 -14
- package/src/providers/registry/entries-extended.ts +19 -0
- 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 +78 -22
- 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/responses/state.ts +32 -29
- package/src/router.ts +8 -321
- package/src/routing/health.ts +37 -22
- package/src/routing/history/build-file.ts +92 -0
- package/src/routing/history/indexer.ts +254 -221
- package/src/routing/history/ingest.ts +242 -0
- package/src/routing/history/rebuild-job.ts +111 -0
- package/src/routing/history/rebuild-protocol.ts +15 -0
- package/src/routing/history/rebuild-worker.ts +34 -0
- package/src/routing/profile.ts +1 -1
- package/src/server/admission-model-scope.ts +21 -1
- package/src/server/antigravity-direct-eligibility.ts +208 -0
- package/src/server/auth-cors.ts +15 -0
- package/src/server/chat-completions.ts +82 -10
- 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 +164 -433
- package/src/server/effort-policy.ts +1 -1
- package/src/server/images.ts +15 -2
- package/src/server/index/desktop-compatibility-startup.ts +56 -0
- 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 +11 -3
- package/src/server/index/startup-warnings.ts +11 -0
- package/src/server/index/websocket-handler.ts +9 -0
- package/src/server/index.ts +14 -11
- package/src/server/inference/advisor-diagnostics.ts +31 -0
- package/src/server/inference/client-encoder-delivery.ts +6 -5
- package/src/server/login-shell-path.ts +146 -0
- package/src/server/management/advisor-routes.ts +202 -0
- package/src/server/management/agent-settings-routes.ts +22 -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 +4 -0
- package/src/server/management/config-routes.ts +77 -6
- package/src/server/management/context.ts +4 -0
- package/src/server/management/cursor-integration-routes.ts +1 -1
- package/src/server/management/desktop-compatibility-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
- package/src/server/management/model-routes.ts +13 -8
- 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/protocol-settings-patch.ts +1 -0
- 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 +21 -1
- package/src/server/management/sibling-guard.ts +1 -0
- package/src/server/management-api.ts +9 -1
- package/src/server/messages-antigravity-direct.ts +195 -0
- package/src/server/messages-native-tool-guard.ts +185 -0
- package/src/server/messages-native.ts +106 -22
- 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 +13 -1
- package/src/server/responses/account-change-state.ts +8 -2
- package/src/server/responses/adapter-continuation.ts +48 -19
- package/src/server/responses/adapter-delivery.ts +43 -8
- package/src/server/responses/adapter-dispatch.ts +48 -4
- package/src/server/responses/advisor-plan-slot.ts +27 -0
- package/src/server/responses/advisor-retention.ts +117 -0
- package/src/server/responses/advisor-slot.ts +368 -0
- package/src/server/responses/code-mode-collab-surface.ts +116 -8
- 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 +12 -1
- package/src/server/responses/core-combo.ts +141 -36
- package/src/server/responses/core-errors.ts +3 -3
- package/src/server/responses/core-lifetime.ts +7 -2
- package/src/server/responses/core-normalize.ts +11 -0
- package/src/server/responses/core-opaque-recovery.ts +35 -0
- package/src/server/responses/core-options.ts +51 -0
- package/src/server/responses/core-pipeline.ts +141 -0
- package/src/server/responses/core-replay.ts +25 -6
- package/src/server/responses/core.ts +3 -116
- 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 +19 -11
- package/src/server/responses/passthrough-dispatch.ts +29 -16
- package/src/server/responses/request-prepare.ts +144 -17
- package/src/server/responses/request-sidecar-auth.ts +2 -2
- package/src/server/responses/request-transport.ts +120 -27
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +166 -38
- package/src/server/responses/sidecar-execution.ts +98 -5
- package/src/server/responses/terminal-guard.ts +4 -17
- package/src/server/responses-undeclared-tool-guard.ts +30 -9
- package/src/server/startup-action-control.ts +16 -2
- package/src/server/startup-health-cache.ts +1 -0
- 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/desktop-startup.ts +14 -0
- package/src/service/guards.ts +1 -61
- package/src/service/health-sentinel-linux.ts +112 -0
- package/src/service/health-sentinel-main.ts +99 -61
- package/src/service/health-sentinel-probe.ts +24 -7
- package/src/service/health.ts +5 -89
- package/src/service/launchd.ts +27 -7
- package/src/service/logging.ts +135 -0
- 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/tray/windows-tray.ps1 +43 -24
- package/src/types/config.ts +124 -6
- package/src/types/opencode-go.ts +61 -0
- package/src/types/opencode-zen.ts +29 -0
- package/src/types/provider.ts +39 -6
- package/src/types/request.ts +38 -2
- package/src/types/tools.ts +2 -0
- package/src/types/wire.ts +29 -37
- package/src/types.ts +1 -0
- package/src/update/async-check.ts +1 -1
- package/src/update/badge.ts +2 -2
- package/src/update/index.ts +88 -15
- package/src/update/job-supervised-restart.ts +107 -0
- package/src/update/job.ts +32 -32
- package/src/update/notify.ts +1 -1
- package/src/update/npm-invocation.mjs +12 -2
- package/src/update/refresh-scheduler.ts +2 -2
- package/src/update/supervised-recovery.d.mts +27 -0
- package/src/update/supervised-recovery.mjs +157 -0
- package/src/usage/cache-diagnostic.ts +2 -0
- package/src/usage/cost.ts +48 -12
- package/src/usage/expected-prices.ts +15 -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/gemini-executor.ts +2 -2
- 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/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/App-CSJvmjNs.css +0 -1
- package/gui/dist/assets/App-CZtqiGcw.js +0 -56
- package/gui/dist/assets/index-BB6uK6tU.css +0 -1
- package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
- package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-route opaque proxy tunnel. The desktop upstream dialer retains its
|
|
3
|
+
* own fixed-target TLS contract.
|
|
4
|
+
* Shares OpenCodex's MIT SOCKS handshake; the caller owns all post-establishment
|
|
5
|
+
* cancellation and socket cleanup. Never replay or implicitly retry directly.
|
|
6
|
+
*/
|
|
7
|
+
import net, { type Socket } from 'node:net';
|
|
8
|
+
import tls from 'node:tls';
|
|
9
|
+
import { socks5Handshake, socks5Credentials, Socks5HandshakeError } from './socks5-handshake';
|
|
10
|
+
|
|
11
|
+
interface RouteEndpoint { host: string; port: number }
|
|
12
|
+
type RouteCredentials = { username?: undefined; password?: undefined } | { username: string; password: string };
|
|
13
|
+
type PlainProxy = { kind?: 'http'; proxyCa?: never };
|
|
14
|
+
type SecureProxy = { kind: 'https'; proxyCa?: string };
|
|
15
|
+
type SocksProxy = { kind: 'socks5' | 'socks5h'; proxyCa?: never };
|
|
16
|
+
export type OpaqueProxyRoute = RouteEndpoint & RouteCredentials & (PlainProxy | SecureProxy | SocksProxy);
|
|
17
|
+
export interface OpaqueTunnelOptions {
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
timeoutMs?: number;
|
|
20
|
+
maxHeaderBytes?: number;
|
|
21
|
+
}
|
|
22
|
+
interface AttemptOptions {
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
timeoutMs: number;
|
|
25
|
+
maxHeaderBytes: number;
|
|
26
|
+
}
|
|
27
|
+
export class TunnelError extends Error {
|
|
28
|
+
override readonly name = 'TunnelError';
|
|
29
|
+
readonly code: string;
|
|
30
|
+
readonly status?: number;
|
|
31
|
+
constructor(code: string, message: string, { status }: { status?: number } = {}) {
|
|
32
|
+
super(message); this.code = code; this.status = status;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const aborted = () => new TunnelError('ABORTED', 'Tunnel establishment cancelled');
|
|
36
|
+
|
|
37
|
+
function validateAuthority(authority: string): string {
|
|
38
|
+
if (typeof authority !== 'string' || !/^(?:\[[0-9a-fA-F:]+\]|[a-zA-Z0-9._-]+):[0-9]+$/.test(authority)) {
|
|
39
|
+
throw new TunnelError('TARGET', 'An explicit host:port authority is required');
|
|
40
|
+
}
|
|
41
|
+
const port = Number(authority.slice(authority.lastIndexOf(':') + 1));
|
|
42
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new TunnelError('TARGET', 'Invalid target port');
|
|
43
|
+
return authority;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function validateOptions(p: OpaqueProxyRoute, timeoutMs: number, maxHeaderBytes: number): void {
|
|
47
|
+
if (!p || Array.isArray(p) || typeof p.host !== 'string' || !p.host || /[\s/@]/.test(p.host)
|
|
48
|
+
|| !Number.isInteger(p.port) || p.port < 1 || p.port > 65535
|
|
49
|
+
|| !['http', 'https', 'socks5', 'socks5h'].includes(p.kind ?? 'http')) throw new TunnelError('CONFIG', 'Invalid proxy route');
|
|
50
|
+
if ((p.username === undefined) !== (p.password === undefined)
|
|
51
|
+
|| (p.username !== undefined && (typeof p.username !== 'string' || typeof p.password !== 'string'
|
|
52
|
+
|| (!p.kind?.startsWith('socks') && p.username.includes(':')) || Buffer.byteLength(p.username + p.password) > 4096))) throw new TunnelError('CONFIG', 'Invalid proxy credentials');
|
|
53
|
+
if ((p.kind ?? 'http').startsWith('socks') && p.username !== undefined
|
|
54
|
+
&& (Buffer.byteLength(p.username) > 255 || Buffer.byteLength(p.password) > 255)) throw new TunnelError('CONFIG', 'SOCKS credentials must each fit in 255 UTF-8 bytes');
|
|
55
|
+
if (p.proxyCa !== undefined && p.kind !== 'https') throw new TunnelError('CONFIG', 'Proxy TLS settings require HTTPS proxy');
|
|
56
|
+
if (p.proxyCa !== undefined && typeof p.proxyCa !== 'string') throw new TunnelError('CONFIG', 'Invalid proxy CA');
|
|
57
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 30000
|
|
58
|
+
|| !Number.isSafeInteger(maxHeaderBytes) || maxHeaderBytes < 64 || maxHeaderBytes > 65536) {
|
|
59
|
+
throw new TunnelError('CONFIG', 'Invalid deadline or header limit');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function connectOne(authority: string, proxy: OpaqueProxyRoute, { signal, timeoutMs, maxHeaderBytes }: AttemptOptions): Promise<Socket> {
|
|
64
|
+
if (proxy.kind === 'socks5' || proxy.kind === 'socks5h') return connectSocks(authority, proxy, { signal, timeoutMs, maxHeaderBytes });
|
|
65
|
+
return new Promise<Socket>((resolve, reject) => {
|
|
66
|
+
if (signal?.aborted) return reject(aborted());
|
|
67
|
+
const secure = proxy.kind === 'https';
|
|
68
|
+
const identity = proxy.host;
|
|
69
|
+
const tlsOptions: tls.ConnectionOptions & { allowHalfOpen: boolean } = {
|
|
70
|
+
host: proxy.host, port: proxy.port, allowHalfOpen: true,
|
|
71
|
+
servername: net.isIP(identity) ? undefined : identity, ca: proxy.proxyCa,
|
|
72
|
+
checkServerIdentity: (_name, cert) => tls.checkServerIdentity(identity, cert),
|
|
73
|
+
rejectUnauthorized: true, ALPNProtocols: ['http/1.1'],
|
|
74
|
+
};
|
|
75
|
+
const socket = secure
|
|
76
|
+
? tls.connect(tlsOptions)
|
|
77
|
+
: net.createConnection({ host: proxy.host, port: proxy.port, allowHalfOpen: true });
|
|
78
|
+
// Keep a defensive observer across the promise-to-owner handoff. Errors still
|
|
79
|
+
// settle the establishment promise or are handled by the relay after handoff.
|
|
80
|
+
socket.on('error', () => {});
|
|
81
|
+
let pending: Buffer = Buffer.alloc(0); let settled = false, tlsReady = !secure;
|
|
82
|
+
const connectedEvent = secure ? 'secureConnect' : 'connect';
|
|
83
|
+
const cleanup = () => {
|
|
84
|
+
clearTimeout(timer); signal?.removeEventListener('abort', onAbort);
|
|
85
|
+
socket.removeListener(connectedEvent, onConnect); socket.removeListener('data', onData);
|
|
86
|
+
socket.removeListener('error', onError); socket.removeListener('end', onEnd); socket.removeListener('close', onClose);
|
|
87
|
+
};
|
|
88
|
+
const fail = (error: Error) => {
|
|
89
|
+
if (settled) return; settled = true; cleanup(); socket.destroy(); reject(error);
|
|
90
|
+
};
|
|
91
|
+
const onAbort = () => fail(aborted());
|
|
92
|
+
const establishmentFailure = (code: string, message: string) => fail(new TunnelError(code, message));
|
|
93
|
+
const onError = () => {
|
|
94
|
+
if (secure && !tlsReady) {
|
|
95
|
+
fail(new TunnelError('PROXY_TLS', 'HTTPS proxy establishment failed'));
|
|
96
|
+
} else establishmentFailure('TRANSPORT', 'HTTP proxy connection failed');
|
|
97
|
+
};
|
|
98
|
+
const onEnd = () => establishmentFailure('EARLY_CLOSE', 'HTTP proxy closed before establishing the tunnel');
|
|
99
|
+
const onClose = onEnd;
|
|
100
|
+
const onConnect = () => {
|
|
101
|
+
tlsReady = true;
|
|
102
|
+
const auth = proxy.username === undefined ? '' : `Proxy-Authorization: Basic ${Buffer.from(`${proxy.username}:${proxy.password}`).toString('base64')}\r\n`;
|
|
103
|
+
socket.write(`CONNECT ${authority} HTTP/1.1\r\nHost: ${authority}\r\n${auth}\r\n`);
|
|
104
|
+
};
|
|
105
|
+
const onData = (chunk: Buffer) => {
|
|
106
|
+
pending = Buffer.concat([pending, chunk]);
|
|
107
|
+
const lineEnd = pending.indexOf('\r\n');
|
|
108
|
+
if (lineEnd >= 0) {
|
|
109
|
+
const firstLine = pending.subarray(0, lineEnd).toString('latin1');
|
|
110
|
+
const firstStatus = /^HTTP\/1\.[01] ([0-9]{3})(?: [\t\x20-\x7e\x80-\xff]*)?$/.exec(firstLine);
|
|
111
|
+
if (!firstStatus) return fail(new TunnelError('PROTOCOL', 'Invalid HTTP proxy status line'));
|
|
112
|
+
const status = Number(firstStatus[1]);
|
|
113
|
+
if (status < 200 || status >= 300) return fail(new TunnelError('REFUSED', 'HTTP proxy refused CONNECT', { status }));
|
|
114
|
+
}
|
|
115
|
+
const end = pending.indexOf('\r\n\r\n');
|
|
116
|
+
if (end < 0) {
|
|
117
|
+
if (pending.length > maxHeaderBytes) fail(new TunnelError('PROTOCOL', 'HTTP proxy response headers exceed limit'));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (end + 4 > maxHeaderBytes) return fail(new TunnelError('PROTOCOL', 'HTTP proxy response headers exceed limit'));
|
|
121
|
+
const lines = pending.subarray(0, end).toString('latin1').split('\r\n');
|
|
122
|
+
const statusLine = lines[0];
|
|
123
|
+
const match = /^HTTP\/1\.[01] ([0-9]{3})(?: [^\r\n]*)?$/.exec(statusLine);
|
|
124
|
+
if (!match) return fail(new TunnelError('PROTOCOL', 'Invalid HTTP proxy status line'));
|
|
125
|
+
const status = Number(match[1]);
|
|
126
|
+
if (lines.slice(1).some(line => !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+:[\t\x20-\x7e\x80-\xff]*$/.test(line))) {
|
|
127
|
+
return fail(new TunnelError('PROTOCOL', 'Invalid HTTP proxy response header'));
|
|
128
|
+
}
|
|
129
|
+
if (status < 200 || status >= 300) return fail(new TunnelError('REFUSED', 'HTTP proxy refused CONNECT', { status }));
|
|
130
|
+
settled = true; socket.pause(); cleanup();
|
|
131
|
+
const head = pending.subarray(end + 4);
|
|
132
|
+
if (head.length) socket.unshift(head);
|
|
133
|
+
resolve(socket);
|
|
134
|
+
};
|
|
135
|
+
const timer = setTimeout(() => establishmentFailure('TIMEOUT', 'HTTP proxy establishment deadline exceeded'), timeoutMs);
|
|
136
|
+
socket.once(connectedEvent, onConnect); socket.on('data', onData);
|
|
137
|
+
socket.once('error', onError); socket.once('end', onEnd); socket.once('close', onClose);
|
|
138
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
139
|
+
if (signal?.aborted) onAbort();
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// The bounded socket reader owns setup cancellation and preserves queued bytes.
|
|
144
|
+
// Framing uses the existing
|
|
145
|
+
// MIT socks5-handshake module; neither shared framing nor its DNS policy changes.
|
|
146
|
+
async function connectSocks(authority: string, proxy: OpaqueProxyRoute, { signal, timeoutMs }: AttemptOptions): Promise<Socket> {
|
|
147
|
+
if (signal?.aborted) throw aborted();
|
|
148
|
+
const socket = net.createConnection({ host: proxy.host, port: proxy.port, allowHalfOpen: true });
|
|
149
|
+
socket.on('error', () => {});
|
|
150
|
+
let buffer: Buffer = Buffer.alloc(0), failure: Error | undefined;
|
|
151
|
+
let waiting: { length: number; resolve: () => void; reject: (error: Error) => void } | undefined;
|
|
152
|
+
const fail = (error: Error) => { if (failure) return; failure = error; const current = waiting; waiting = undefined; current?.reject(error); socket.destroy(); };
|
|
153
|
+
const transportError = (code: string, message: string) => fail(new TunnelError(code, message));
|
|
154
|
+
const onData = (chunk: Buffer) => {
|
|
155
|
+
// Consume at most the bytes needed by a bounded handshake read, then pause.
|
|
156
|
+
// A valid CONNECT reply can share a packet with arbitrary early target data;
|
|
157
|
+
// that remainder is not a malformed/oversized handshake and must survive.
|
|
158
|
+
socket.pause();
|
|
159
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
160
|
+
if (waiting && buffer.length >= waiting.length) { const current = waiting; waiting = undefined; current.resolve(); }
|
|
161
|
+
else if (waiting) socket.resume();
|
|
162
|
+
};
|
|
163
|
+
const onError = () => transportError('TRANSPORT', 'SOCKS proxy connection failed');
|
|
164
|
+
const onClose = () => transportError('EARLY_CLOSE', 'SOCKS proxy closed before tunnel establishment');
|
|
165
|
+
const onAbort = () => fail(aborted());
|
|
166
|
+
const timer = setTimeout(() => transportError('TIMEOUT', 'SOCKS establishment deadline exceeded'), timeoutMs);
|
|
167
|
+
socket.on('data', onData); socket.once('error', onError); socket.once('end', onClose); socket.once('close', onClose);
|
|
168
|
+
socket.pause();
|
|
169
|
+
signal?.addEventListener('abort', onAbort, { once: true }); if (signal?.aborted) onAbort();
|
|
170
|
+
const reader = {
|
|
171
|
+
write(bytes: Uint8Array): void { if (failure) throw failure; socket.write(bytes); },
|
|
172
|
+
async readExact(length: number): Promise<Buffer> {
|
|
173
|
+
if (failure) throw failure;
|
|
174
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > 1024) throw new TunnelError('PROTOCOL', 'Invalid handshake read size');
|
|
175
|
+
if (buffer.length < length) await new Promise<void>((resolve, reject) => {
|
|
176
|
+
if (waiting) { reject(new TunnelError('PROTOCOL', 'Concurrent handshake read')); return; }
|
|
177
|
+
waiting = { length, resolve, reject };
|
|
178
|
+
socket.resume();
|
|
179
|
+
});
|
|
180
|
+
if (failure) throw failure;
|
|
181
|
+
const result = buffer.subarray(0, length); buffer = buffer.subarray(length); return result;
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
try {
|
|
185
|
+
const credentialsUrl = new URL('socks5://fixture.invalid');
|
|
186
|
+
if (proxy.username !== undefined) { credentialsUrl.username = encodeURIComponent(proxy.username); credentialsUrl.password = encodeURIComponent(proxy.password); }
|
|
187
|
+
const colon = authority.lastIndexOf(':');
|
|
188
|
+
const target = { host: authority.slice(0, colon).replace(/^\[|\]$/g, ''), port: Number(authority.slice(colon + 1)) };
|
|
189
|
+
await socks5Handshake(reader, target, socks5Credentials(credentialsUrl), signal);
|
|
190
|
+
if (failure) throw failure;
|
|
191
|
+
if (signal?.aborted || socket.destroyed) throw aborted();
|
|
192
|
+
socket.pause(); if (buffer.length) socket.unshift(buffer); buffer = Buffer.alloc(0);
|
|
193
|
+
return socket;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
socket.destroy();
|
|
196
|
+
if (error instanceof Socks5HandshakeError) throw new TunnelError('SOCKS_REFUSED', 'SOCKS authentication, policy, or protocol refusal');
|
|
197
|
+
throw error;
|
|
198
|
+
} finally {
|
|
199
|
+
clearTimeout(timer); signal?.removeEventListener('abort', onAbort);
|
|
200
|
+
socket.removeListener('data', onData); socket.removeListener('error', onError);
|
|
201
|
+
socket.removeListener('end', onClose); socket.removeListener('close', onClose);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Establish only. There is no application data, implicit DIRECT, or replay here.
|
|
206
|
+
* HTTP/HTTPS CONNECT and SOCKS5/SOCKS5h yield an opaque raw target tunnel.
|
|
207
|
+
*/
|
|
208
|
+
export async function dialTunnel(authority: string, proxy: OpaqueProxyRoute, options: OpaqueTunnelOptions = {}): Promise<Socket> {
|
|
209
|
+
const { signal, timeoutMs = 10000, maxHeaderBytes = 65536 } = options;
|
|
210
|
+
validateAuthority(authority);
|
|
211
|
+
validateOptions(proxy, timeoutMs, maxHeaderBytes);
|
|
212
|
+
if (signal?.aborted) throw aborted();
|
|
213
|
+
// Snapshot the selected route. No alternate proxy or direct fallback exists.
|
|
214
|
+
const socket = await connectOne(authority, { ...proxy }, { signal, timeoutMs, maxHeaderBytes });
|
|
215
|
+
if (signal?.aborted || socket.destroyed) { socket.destroy(); throw aborted(); }
|
|
216
|
+
return socket;
|
|
217
|
+
}
|
|
@@ -68,7 +68,7 @@ export interface PackageTreeIntegrityOptions {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
export type ObservePackageTree = () => PackageTreeObservation | null;
|
|
71
|
-
export type PackageTreeRuntimeInstall = "bun" | "mise" | "npm" | "pnpm" | "source";
|
|
71
|
+
export type PackageTreeRuntimeInstall = "bun" | "mise" | "npm" | "pnpm" | "source" | "standalone";
|
|
72
72
|
|
|
73
73
|
const packageManifestUrl = new URL("../../package.json", import.meta.url);
|
|
74
74
|
|
|
@@ -323,7 +323,7 @@ export function createRuntimePackageTreeIntegrityGuard(
|
|
|
323
323
|
now: () => number = Date.now,
|
|
324
324
|
options: PackageTreeIntegrityOptions = {},
|
|
325
325
|
): PackageTreeIntegrityGuard {
|
|
326
|
-
if (installer === "source" || isStandaloneBinary()) {
|
|
326
|
+
if (installer === "source" || installer === "standalone" || isStandaloneBinary()) {
|
|
327
327
|
return { status: () => ({ ok: true }), dispose: () => {} };
|
|
328
328
|
}
|
|
329
329
|
// The restart replacement spawns process.execPath, the bundled Bun in an npm install. Read it
|
package/src/lib/redact.ts
CHANGED
|
@@ -7,7 +7,7 @@ export const REDACTED_SECRET = "[REDACTED]";
|
|
|
7
7
|
* credentials over an unsafe channel (e.g. plaintext non-loopback HTTP) rather than
|
|
8
8
|
* re-deriving a narrower local list.
|
|
9
9
|
*/
|
|
10
|
-
export const SENSITIVE_KEY_PATTERN = /^(?:authorization|proxy-authorization|cookie|set-cookie|set-cookie2|api[-_]?key|x-api-key|x-goog-api-key|x-amz-security-token|copilot[-_]?session[-_]?token|session[-_]?token|access[-_]?token|refresh[-_]?token|id[-_]?token|token|secret|client[-_]?secret|password|profile[-_]?arn|exa[-_]?api[-_]?key)$/i;
|
|
10
|
+
export const SENSITIVE_KEY_PATTERN = /^(?:authorization|proxy-authorization|cookie|set-cookie|set-cookie2|api[-_]?key|x-api-key|x-goog-api-key|x-amz-security-token|x-dsh-auth-token|copilot[-_]?session[-_]?token|session[-_]?token|access[-_]?token|refresh[-_]?token|id[-_]?token|token|secret|client[-_]?secret|password|profile[-_]?arn|exa[-_]?api[-_]?key)$/i;
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Colon-labelled credential headers echoed back inside an error body
|
|
@@ -40,7 +40,7 @@ export const SENSITIVE_KEY_PATTERN = /^(?:authorization|proxy-authorization|cook
|
|
|
40
40
|
// Every letter position also accepts \u0001, the placeholder the fold emits for
|
|
41
41
|
// an unresolved HTML named reference: `authorⅈzation` is the label with one
|
|
42
42
|
// character we cannot name, and that is still the label.
|
|
43
|
-
const CREDENTIAL_HEADER_LABEL_RAW = "copilot[_-]?session[_-]?token|session[_-]?token|sessionToken|x-api-key|x-goog-api-key|x-amz-security-token|api[_-]?key|apiKey|exa[_-]?api[_-]?key|exaApiKey|access[_-]?token|accessToken|refresh[_-]?token|refreshToken|id[_-]?token|client[_-]?secret|clientSecret|authorization|proxy-authorization|cookie|set-cookie|password|secret|token";
|
|
43
|
+
const CREDENTIAL_HEADER_LABEL_RAW = "copilot[_-]?session[_-]?token|session[_-]?token|sessionToken|x-api-key|x-goog-api-key|x-amz-security-token|x-dsh-auth-token|api[_-]?key|apiKey|exa[_-]?api[_-]?key|exaApiKey|access[_-]?token|accessToken|refresh[_-]?token|refreshToken|id[_-]?token|client[_-]?secret|clientSecret|authorization|proxy-authorization|cookie|set-cookie|password|secret|token";
|
|
44
44
|
|
|
45
45
|
const CREDENTIAL_HEADER_LABEL = CREDENTIAL_HEADER_LABEL_RAW
|
|
46
46
|
.replace(/(?<![\[\\])([A-Za-z])(?![\]\-])/g, "[$1\u0001]");
|
package/src/lib/socks5-fetch.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import net, { type Socket } from "node:net";
|
|
2
2
|
import tls, { type TLSSocket } from "node:tls";
|
|
3
3
|
import { classifyContentCoding, isNullBodyStatus } from "./http-response-semantics";
|
|
4
|
+
import { socks5Credentials, socks5Handshake, Socks5HandshakeError } from "./socks5-handshake";
|
|
4
5
|
|
|
5
6
|
const DEFAULT_SOCKS5_PORT = 1080;
|
|
6
7
|
const SOCKS5_CONNECT_TIMEOUT_MS = 30_000;
|
|
@@ -10,11 +11,6 @@ const MAX_BODY_SLICE_BYTES = 64 * 1024;
|
|
|
10
11
|
const MAX_DECODED_BODY_BYTES = 32 * 1024 * 1024;
|
|
11
12
|
const MAX_STREAM_DECODE_EXPANSION_RATIO = 128;
|
|
12
13
|
const SOCKS5_VERSION = 0x05;
|
|
13
|
-
const SOCKS5_NO_AUTH = 0x00;
|
|
14
|
-
const SOCKS5_USER_PASS = 0x02;
|
|
15
|
-
const SOCKS5_CONNECT = 0x01;
|
|
16
|
-
const SOCKS5_DOMAIN = 0x03;
|
|
17
|
-
const SOCKS5_SUCCESS = 0x00;
|
|
18
14
|
const CRLF = Buffer.from("\r\n");
|
|
19
15
|
const HEADER_END = Buffer.from("\r\n\r\n");
|
|
20
16
|
|
|
@@ -22,24 +18,6 @@ export class Socks5FetchError extends Error {
|
|
|
22
18
|
override readonly name = "Socks5FetchError";
|
|
23
19
|
}
|
|
24
20
|
|
|
25
|
-
function proxyCredentials(proxy: URL): { username?: Uint8Array; password?: Uint8Array } {
|
|
26
|
-
if (!proxy.username && !proxy.password) return {};
|
|
27
|
-
let username: string;
|
|
28
|
-
let password: string;
|
|
29
|
-
try {
|
|
30
|
-
username = decodeURIComponent(proxy.username);
|
|
31
|
-
password = decodeURIComponent(proxy.password);
|
|
32
|
-
} catch {
|
|
33
|
-
throw new Socks5FetchError("SOCKS5 proxy credentials contain invalid percent encoding");
|
|
34
|
-
}
|
|
35
|
-
const usernameBytes = new TextEncoder().encode(username);
|
|
36
|
-
const passwordBytes = new TextEncoder().encode(password);
|
|
37
|
-
if (usernameBytes.byteLength > 255 || passwordBytes.byteLength > 255) {
|
|
38
|
-
throw new Socks5FetchError("SOCKS5 proxy credentials must each fit in 255 UTF-8 bytes");
|
|
39
|
-
}
|
|
40
|
-
return { username: usernameBytes, password: passwordBytes };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
21
|
function validateProxy(proxy: string): URL {
|
|
44
22
|
let parsed: URL;
|
|
45
23
|
try {
|
|
@@ -55,7 +33,10 @@ function validateProxy(proxy: string): URL {
|
|
|
55
33
|
throw new Socks5FetchError("SOCKS5 proxy port is invalid");
|
|
56
34
|
}
|
|
57
35
|
if (parsed.search || parsed.hash) throw new Socks5FetchError("SOCKS5 proxy URL must not contain a query or fragment");
|
|
58
|
-
|
|
36
|
+
try { socks5Credentials(parsed); } catch (error) {
|
|
37
|
+
if (error instanceof Socks5HandshakeError) throw new Socks5FetchError(error.message);
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
59
40
|
return parsed;
|
|
60
41
|
}
|
|
61
42
|
|
|
@@ -130,6 +111,14 @@ class SocketReader {
|
|
|
130
111
|
socket.resume();
|
|
131
112
|
}
|
|
132
113
|
|
|
114
|
+
write(bytes: Uint8Array): void {
|
|
115
|
+
this.socket.write(bytes);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
readExact(bytes: number, signal?: AbortSignal): Promise<Buffer> {
|
|
119
|
+
return this.read(bytes, signal);
|
|
120
|
+
}
|
|
121
|
+
|
|
133
122
|
private readonly onData = (chunk: Buffer | string): void => {
|
|
134
123
|
const value = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
135
124
|
if (this.anyWaiters.length > 0) {
|
|
@@ -263,7 +252,7 @@ class SocketReader {
|
|
|
263
252
|
|
|
264
253
|
async function socks5Connect(proxy: string, target: URL, signal?: AbortSignal): Promise<Socket> {
|
|
265
254
|
const parsedProxy = validateProxy(proxy);
|
|
266
|
-
const credentials =
|
|
255
|
+
const credentials = socks5Credentials(parsedProxy);
|
|
267
256
|
const proxyHost = parsedProxy.hostname.replace(/^\[|\]$/g, "");
|
|
268
257
|
const socket = await connectSocket(proxyHost, Number(parsedProxy.port) || DEFAULT_SOCKS5_PORT, signal);
|
|
269
258
|
socket.setTimeout(SOCKS5_CONNECT_TIMEOUT_MS, () => {
|
|
@@ -271,49 +260,12 @@ async function socks5Connect(proxy: string, target: URL, signal?: AbortSignal):
|
|
|
271
260
|
});
|
|
272
261
|
const reader = new SocketReader(socket);
|
|
273
262
|
try {
|
|
274
|
-
|
|
275
|
-
socket.write(Buffer.from([SOCKS5_VERSION, methods.byteLength, ...methods]));
|
|
276
|
-
const greeting = await reader.read(2, signal);
|
|
277
|
-
if (greeting[0] !== SOCKS5_VERSION) throw new Socks5FetchError("SOCKS5 proxy returned an invalid greeting");
|
|
278
|
-
if (greeting[1] === SOCKS5_USER_PASS && credentials.username && credentials.password) {
|
|
279
|
-
socket.write(Buffer.from([
|
|
280
|
-
0x01,
|
|
281
|
-
credentials.username.byteLength,
|
|
282
|
-
...credentials.username,
|
|
283
|
-
credentials.password.byteLength,
|
|
284
|
-
...credentials.password,
|
|
285
|
-
]));
|
|
286
|
-
const auth = await reader.read(2, signal);
|
|
287
|
-
if (auth[0] !== 0x01 || auth[1] !== 0x00) throw new Socks5FetchError("SOCKS5 proxy authentication failed");
|
|
288
|
-
} else if (greeting[1] !== SOCKS5_NO_AUTH) {
|
|
289
|
-
throw new Socks5FetchError("SOCKS5 proxy does not accept an offered authentication method");
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const hostname = new TextEncoder().encode(target.hostname);
|
|
293
|
-
if (hostname.byteLength > 255) throw new Socks5FetchError("SOCKS5 target hostname is too long");
|
|
294
|
-
const port = targetPort(target);
|
|
295
|
-
socket.write(Buffer.from([
|
|
296
|
-
SOCKS5_VERSION,
|
|
297
|
-
SOCKS5_CONNECT,
|
|
298
|
-
0x00,
|
|
299
|
-
SOCKS5_DOMAIN,
|
|
300
|
-
hostname.byteLength,
|
|
301
|
-
...hostname,
|
|
302
|
-
port >> 8,
|
|
303
|
-
port & 0xff,
|
|
304
|
-
]));
|
|
305
|
-
const reply = await reader.read(4, signal);
|
|
306
|
-
if (reply[0] !== SOCKS5_VERSION) throw new Socks5FetchError("SOCKS5 proxy returned an invalid connect response");
|
|
307
|
-
if (reply[1] !== SOCKS5_SUCCESS) throw new Socks5FetchError(`SOCKS5 proxy refused the connection (code ${reply[1]})`);
|
|
308
|
-
if (reply[2] !== 0x00 || ![0x01, SOCKS5_DOMAIN, 0x04].includes(reply[3]!)) {
|
|
309
|
-
throw new Socks5FetchError("SOCKS5 proxy returned an invalid address type or reserved byte");
|
|
310
|
-
}
|
|
311
|
-
const addressLength = reply[3] === 0x01 ? 4 : reply[3] === SOCKS5_DOMAIN ? (await reader.read(1, signal))[0]! : 16;
|
|
312
|
-
await reader.read(addressLength + 2, signal);
|
|
263
|
+
await socks5Handshake(reader, { host: target.hostname, port: targetPort(target) }, credentials, signal);
|
|
313
264
|
socket.setTimeout(0);
|
|
314
265
|
return socket;
|
|
315
266
|
} catch (error) {
|
|
316
267
|
socket.destroy();
|
|
268
|
+
if (error instanceof Socks5HandshakeError) throw new Socks5FetchError(error.message);
|
|
317
269
|
throw error;
|
|
318
270
|
} finally {
|
|
319
271
|
reader.dispose();
|
|
@@ -341,25 +293,50 @@ function requestHeaders(request: Request, target: URL): { text: string; chunked:
|
|
|
341
293
|
|
|
342
294
|
async function secureSocket(socket: Socket, target: URL, signal: AbortSignal): Promise<TLSSocket> {
|
|
343
295
|
return new Promise((resolve, reject) => {
|
|
296
|
+
if (signal.aborted) {
|
|
297
|
+
socket.destroy();
|
|
298
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error("The operation was aborted"));
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
344
301
|
const tlsSocket = tls.connect({
|
|
345
302
|
socket,
|
|
346
303
|
servername: target.hostname,
|
|
347
304
|
rejectUnauthorized: true,
|
|
348
305
|
});
|
|
349
|
-
|
|
306
|
+
let settled = false;
|
|
307
|
+
// Absolute deadline: a peer sending occasional TLS bytes must not extend the handshake.
|
|
308
|
+
const timer = setTimeout(() => {
|
|
309
|
+
fail(new Socks5FetchError("SOCKS5 TLS handshake timed out"));
|
|
310
|
+
}, SOCKS5_CONNECT_TIMEOUT_MS);
|
|
311
|
+
const cleanup = () => {
|
|
312
|
+
clearTimeout(timer);
|
|
313
|
+
signal.removeEventListener("abort", onAbort);
|
|
314
|
+
tlsSocket.removeListener("secureConnect", onSecureConnect);
|
|
315
|
+
tlsSocket.removeListener("error", onError);
|
|
316
|
+
tlsSocket.removeListener("close", onClose);
|
|
317
|
+
};
|
|
318
|
+
const fail = (error: Error) => {
|
|
319
|
+
if (settled) return;
|
|
320
|
+
settled = true;
|
|
321
|
+
cleanup();
|
|
322
|
+
// No error argument: pending event observers must not receive a second error.
|
|
350
323
|
tlsSocket.destroy();
|
|
351
|
-
reject(
|
|
324
|
+
reject(error);
|
|
352
325
|
};
|
|
326
|
+
const onAbort = () => fail(signal.reason instanceof Error ? signal.reason : new Error("The operation was aborted"));
|
|
327
|
+
const onError = (error: Error) => fail(error);
|
|
328
|
+
const onClose = () => fail(new Socks5FetchError("SOCKS5 upstream closed during TLS handshake"));
|
|
353
329
|
const onSecureConnect = () => {
|
|
354
|
-
|
|
330
|
+
if (settled) return;
|
|
331
|
+
settled = true;
|
|
332
|
+
cleanup();
|
|
355
333
|
resolve(tlsSocket);
|
|
356
334
|
};
|
|
357
335
|
tlsSocket.once("secureConnect", onSecureConnect);
|
|
358
|
-
tlsSocket.once("error",
|
|
359
|
-
|
|
360
|
-
reject(error);
|
|
361
|
-
});
|
|
336
|
+
tlsSocket.once("error", onError);
|
|
337
|
+
tlsSocket.once("close", onClose);
|
|
362
338
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
339
|
+
if (signal.aborted) onAbort();
|
|
363
340
|
});
|
|
364
341
|
}
|
|
365
342
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SOCKS5 handshake both raw outbound transports share: method negotiation
|
|
3
|
+
* (RFC 1928), the optional RFC 1929 username/password subnegotiation, and the
|
|
4
|
+
* CONNECT exchange.
|
|
5
|
+
*
|
|
6
|
+
* `src/lib/socks5-fetch.ts` (the HTTP tunnel) and
|
|
7
|
+
* `src/lib/desktop-upstream-tunnel.ts` (the native desktop relay's raw
|
|
8
|
+
* dial) each own their socket, their timeouts, and what happens after CONNECT;
|
|
9
|
+
* this module only frames bytes through the reader each hands it, so a
|
|
10
|
+
* credentialed `socks5://` proxy URL authenticates identically whether the caller
|
|
11
|
+
* is a fetch or a raw WebSocket upgrade. When the URL carries credentials the
|
|
12
|
+
* greeting offers NO-AUTH alongside USER-PASS, which keeps an unauthenticated
|
|
13
|
+
* proxy working for a caller that configured more than it needed.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export class Socks5HandshakeError extends Error {
|
|
17
|
+
override readonly name = "Socks5HandshakeError";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const SOCKS5_VERSION = 0x05;
|
|
21
|
+
const SOCKS5_NO_AUTH = 0x00;
|
|
22
|
+
const SOCKS5_USER_PASS = 0x02;
|
|
23
|
+
const SOCKS5_CONNECT = 0x01;
|
|
24
|
+
const SOCKS5_DOMAIN = 0x03;
|
|
25
|
+
const SOCKS5_SUCCESS = 0x00;
|
|
26
|
+
|
|
27
|
+
/** RFC 1929 credentials decoded from the proxy URL, each bounded to one length byte. */
|
|
28
|
+
export interface Socks5Credentials {
|
|
29
|
+
username?: Uint8Array;
|
|
30
|
+
password?: Uint8Array;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function socks5Credentials(proxy: URL): Socks5Credentials {
|
|
34
|
+
if (!proxy.username && !proxy.password) return {};
|
|
35
|
+
let username: string;
|
|
36
|
+
let password: string;
|
|
37
|
+
try {
|
|
38
|
+
username = decodeURIComponent(proxy.username);
|
|
39
|
+
password = decodeURIComponent(proxy.password);
|
|
40
|
+
} catch {
|
|
41
|
+
throw new Socks5HandshakeError("SOCKS5 proxy credentials contain invalid percent encoding");
|
|
42
|
+
}
|
|
43
|
+
const usernameBytes = new TextEncoder().encode(username);
|
|
44
|
+
const passwordBytes = new TextEncoder().encode(password);
|
|
45
|
+
if (usernameBytes.byteLength > 255 || passwordBytes.byteLength > 255) {
|
|
46
|
+
throw new Socks5HandshakeError("SOCKS5 proxy credentials must each fit in 255 UTF-8 bytes");
|
|
47
|
+
}
|
|
48
|
+
return { username: usernameBytes, password: passwordBytes };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The slice of a transport's socket reader the handshake needs: exact-length reads
|
|
53
|
+
* that reject on close, on error, or on the transport's own timeout, plus writes.
|
|
54
|
+
* Bytes read past a step stay queued in the transport's reader; the handshake never
|
|
55
|
+
* touches the socket itself.
|
|
56
|
+
*/
|
|
57
|
+
export interface Socks5HandshakeReader {
|
|
58
|
+
write(bytes: Uint8Array): void;
|
|
59
|
+
readExact(bytes: number, signal?: AbortSignal): Promise<Uint8Array>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface Socks5Target {
|
|
63
|
+
host: string;
|
|
64
|
+
port: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Negotiate methods, authenticate when the proxy picks USER-PASS, and CONNECT to `target`. */
|
|
68
|
+
export async function socks5Handshake(
|
|
69
|
+
reader: Socks5HandshakeReader,
|
|
70
|
+
target: Socks5Target,
|
|
71
|
+
credentials: Socks5Credentials,
|
|
72
|
+
signal?: AbortSignal,
|
|
73
|
+
): Promise<void> {
|
|
74
|
+
const hostBytes = Buffer.from(target.host, "utf8");
|
|
75
|
+
if (hostBytes.byteLength > 255) throw new Socks5HandshakeError("SOCKS5 target hostname is too long");
|
|
76
|
+
const methods = credentials.username ? [SOCKS5_NO_AUTH, SOCKS5_USER_PASS] : [SOCKS5_NO_AUTH];
|
|
77
|
+
reader.write(Buffer.from([SOCKS5_VERSION, methods.length, ...methods]));
|
|
78
|
+
const greeting = await reader.readExact(2, signal);
|
|
79
|
+
if (greeting[0] !== SOCKS5_VERSION) throw new Socks5HandshakeError("SOCKS5 proxy returned an invalid greeting");
|
|
80
|
+
if (greeting[1] === SOCKS5_USER_PASS && credentials.username && credentials.password) {
|
|
81
|
+
reader.write(Buffer.from([
|
|
82
|
+
0x01,
|
|
83
|
+
credentials.username.byteLength,
|
|
84
|
+
...credentials.username,
|
|
85
|
+
credentials.password.byteLength,
|
|
86
|
+
...credentials.password,
|
|
87
|
+
]));
|
|
88
|
+
const auth = await reader.readExact(2, signal);
|
|
89
|
+
if (auth[0] !== 0x01 || auth[1] !== 0x00) throw new Socks5HandshakeError("SOCKS5 proxy authentication failed");
|
|
90
|
+
} else if (greeting[1] !== SOCKS5_NO_AUTH) {
|
|
91
|
+
throw new Socks5HandshakeError("SOCKS5 proxy does not accept an offered authentication method");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
reader.write(Buffer.from([
|
|
95
|
+
SOCKS5_VERSION,
|
|
96
|
+
SOCKS5_CONNECT,
|
|
97
|
+
0x00,
|
|
98
|
+
SOCKS5_DOMAIN,
|
|
99
|
+
hostBytes.byteLength,
|
|
100
|
+
...hostBytes,
|
|
101
|
+
target.port >> 8,
|
|
102
|
+
target.port & 0xff,
|
|
103
|
+
]));
|
|
104
|
+
const reply = await reader.readExact(4, signal);
|
|
105
|
+
if (reply[0] !== SOCKS5_VERSION) throw new Socks5HandshakeError("SOCKS5 proxy returned an invalid connect response");
|
|
106
|
+
if (reply[1] !== SOCKS5_SUCCESS) throw new Socks5HandshakeError(`SOCKS5 proxy refused the connection (code ${reply[1]})`);
|
|
107
|
+
if (reply[2] !== 0x00 || ![0x01, SOCKS5_DOMAIN, 0x04].includes(reply[3]!)) {
|
|
108
|
+
throw new Socks5HandshakeError("SOCKS5 proxy returned an invalid address type or reserved byte");
|
|
109
|
+
}
|
|
110
|
+
const addressLength = reply[3] === 0x01 ? 4 : reply[3] === SOCKS5_DOMAIN ? (await reader.readExact(1, signal))[0]! : 16;
|
|
111
|
+
await reader.readExact(addressLength + 2, signal);
|
|
112
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isNonReplayableResponse, markResponseNonReplayable } from "./upstream-retry";
|
|
2
|
+
|
|
1
3
|
export const TRANSLATOR_MAX_CALL_ARGUMENT_BYTES = 2 * 1024 * 1024;
|
|
2
4
|
export const TRANSLATOR_MAX_TURN_BYTES = 32 * 1024 * 1024;
|
|
3
5
|
export const TRANSLATOR_MAX_SSE_EVENT_BYTES = 32 * 1024 * 1024;
|
|
@@ -410,5 +412,9 @@ export function finalizeTranslatorBudgetResponse(response: Response, budget: Tra
|
|
|
410
412
|
});
|
|
411
413
|
if (signal?.aborted) finalize();
|
|
412
414
|
else signal?.addEventListener("abort", finalize, { once: true });
|
|
413
|
-
|
|
415
|
+
const finalizedResponse = new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
|
|
416
|
+
// The rebuild above drops identity markers. A refusal that must not be replayed
|
|
417
|
+
// stays non-replayable through accounting finalization.
|
|
418
|
+
if (isNonReplayableResponse(response)) markResponseNonReplayable(finalizedResponse);
|
|
419
|
+
return finalizedResponse;
|
|
414
420
|
}
|
|
@@ -96,7 +96,7 @@ function headroomOf(provider: string, accountId: string, requestedModelId?: stri
|
|
|
96
96
|
const family = classifyModelFamilyForQuota(provider, requestedModelId);
|
|
97
97
|
if (family) {
|
|
98
98
|
const percents = (quota.customWindows ?? [])
|
|
99
|
-
.filter(window => windowMatchesFamily(window.label, family))
|
|
99
|
+
.filter(window => window.modelId === undefined && windowMatchesFamily(window.label, family))
|
|
100
100
|
.map(window => window.percent)
|
|
101
101
|
.filter((value): value is number => typeof value === "number");
|
|
102
102
|
if (percents.length === 0) return null;
|
|
@@ -106,7 +106,7 @@ function headroomOf(provider: string, accountId: string, requestedModelId?: stri
|
|
|
106
106
|
quota.fiveHourPercent,
|
|
107
107
|
quota.weeklyPercent,
|
|
108
108
|
quota.monthlyPercent,
|
|
109
|
-
...(quota.customWindows ?? []).map(window => window.percent),
|
|
109
|
+
...(quota.customWindows ?? []).filter(window => window.modelId === undefined).map(window => window.percent),
|
|
110
110
|
].filter((value): value is number => typeof value === "number");
|
|
111
111
|
if (percents.length === 0) return null;
|
|
112
112
|
return 100 - Math.max(...percents);
|
|
@@ -140,6 +140,73 @@ export function isAccountQuotaExhausted(
|
|
|
140
140
|
return headroom !== null && headroom <= 0;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Earliest future reset timestamp (in ms) for the relevant quota window, or null if unknown.
|
|
145
|
+
* Prefers weekly reset when multiple windows exist so expiring allowance is consumed first.
|
|
146
|
+
*/
|
|
147
|
+
export function accountResetTimestamp(
|
|
148
|
+
provider: string,
|
|
149
|
+
accountId: string,
|
|
150
|
+
requestedModelId?: string | null,
|
|
151
|
+
now = Date.now(),
|
|
152
|
+
): number | null {
|
|
153
|
+
const quota = getCachedProviderAccountQuota(provider, accountId);
|
|
154
|
+
if (!quota) return null;
|
|
155
|
+
const family = classifyModelFamilyForQuota(provider, requestedModelId);
|
|
156
|
+
const windows = (quota.customWindows ?? []).filter(w => !family || windowMatchesFamily(w.label, family));
|
|
157
|
+
const weekly = windows.find(w => /week/i.test(w.label));
|
|
158
|
+
let rawReset = weekly?.resetAt ?? quota.weeklyResetAt;
|
|
159
|
+
if (rawReset === undefined && provider !== "google-antigravity" && provider !== "anthropic") {
|
|
160
|
+
// Providers without a weekly cadence: prefer an explicitly 5h-labeled window before
|
|
161
|
+
// falling back to first-window order.
|
|
162
|
+
const fiveHour = windows.find(w => /5h|five/i.test(w.label)) ?? windows[0];
|
|
163
|
+
rawReset = fiveHour?.resetAt ?? quota.fiveHourResetAt;
|
|
164
|
+
} else if (rawReset === undefined) {
|
|
165
|
+
rawReset = windows[0]?.resetAt ?? quota.fiveHourResetAt;
|
|
166
|
+
}
|
|
167
|
+
if (typeof rawReset !== "number" || !Number.isFinite(rawReset)) return null;
|
|
168
|
+
const ms = rawReset < 1e11 ? rawReset * 1000 : rawReset;
|
|
169
|
+
return ms > now ? ms : null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Rank candidates by soonest reset timestamp first among healthy accounts.
|
|
174
|
+
* Accounts whose quota will reset earliest get priority so allowances do not expire unused.
|
|
175
|
+
*
|
|
176
|
+
* When a passive provider has incomplete quota evidence (some ring members with no
|
|
177
|
+
* cached record), reset timestamps are unreliable: an unknown account converts to
|
|
178
|
+
* Infinity and always loses to a known near-exhausted one. Fall back to headroom
|
|
179
|
+
* order instead of inventing a reset ordering.
|
|
180
|
+
*/
|
|
181
|
+
export function rankAccountsByResetFirst(
|
|
182
|
+
provider: string,
|
|
183
|
+
ring: readonly string[],
|
|
184
|
+
requestedModelId?: string | null,
|
|
185
|
+
now = Date.now(),
|
|
186
|
+
accounts?: ReadonlyMap<string, ProviderAccount>,
|
|
187
|
+
): string[] {
|
|
188
|
+
if (ring.length < 2) return [...ring];
|
|
189
|
+
const ranked = rankAccountsByHeadroom(provider, ring, requestedModelId, accounts);
|
|
190
|
+
const healthy = ranked.filter(id => !isAccountQuotaExhausted(provider, id, requestedModelId, accounts?.get(id)));
|
|
191
|
+
if (healthy.length < 2) return ranked;
|
|
192
|
+
if (
|
|
193
|
+
hasPassiveAccountQuota(provider)
|
|
194
|
+
&& !ring.every(id => headroomOf(provider, id, requestedModelId, accounts?.get(id)) !== null)
|
|
195
|
+
) return ranked;
|
|
196
|
+
|
|
197
|
+
const withReset = healthy.map((id, index) => ({
|
|
198
|
+
id,
|
|
199
|
+
resetAt: accountResetTimestamp(provider, id, requestedModelId, now) ?? Number.POSITIVE_INFINITY,
|
|
200
|
+
headroom: headroomOf(provider, id, requestedModelId, accounts?.get(id)) ?? 0,
|
|
201
|
+
index,
|
|
202
|
+
}));
|
|
203
|
+
|
|
204
|
+
withReset.sort((a, b) => (a.resetAt - b.resetAt) || (b.headroom - a.headroom) || (a.index - b.index));
|
|
205
|
+
const sortedHealthy = withReset.map(r => r.id);
|
|
206
|
+
const exhausted = ranked.filter(id => isAccountQuotaExhausted(provider, id, requestedModelId, accounts?.get(id)));
|
|
207
|
+
return [...sortedHealthy, ...exhausted];
|
|
208
|
+
}
|
|
209
|
+
|
|
143
210
|
/**
|
|
144
211
|
* Order candidates best-first.
|
|
145
212
|
*
|