@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { accessSync, constants, lstatSync, readlinkSync, realpathSync, statSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { posix, win32 } from "node:path";
|
|
5
5
|
import { performance } from "node:perf_hooks";
|
|
@@ -7,6 +7,7 @@ import { isRealBunBinary } from "./bun-binary-validator.mjs";
|
|
|
7
7
|
|
|
8
8
|
const STABLE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
9
9
|
const MAX_VERSION_BYTES = 4096;
|
|
10
|
+
const MAX_SYMLINK_EXPANSIONS = 40;
|
|
10
11
|
const IDENTITY_PROBE = "process.stdout.write(String(typeof Bun==='object'&&Bun.version))";
|
|
11
12
|
|
|
12
13
|
function stableVersion(value) {
|
|
@@ -17,6 +18,62 @@ function stableVersion(value) {
|
|
|
17
18
|
return numbers.every(Number.isSafeInteger) ? numbers : null;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
function trustedPosixCandidate(candidate, canonical, io) {
|
|
22
|
+
const lstat = io.lstat ?? lstatSync;
|
|
23
|
+
const readlink = io.readlink ?? readlinkSync;
|
|
24
|
+
const uid = io.geteuid?.() ?? process.geteuid?.();
|
|
25
|
+
if (!Number.isSafeInteger(uid) || uid < 0) return false;
|
|
26
|
+
|
|
27
|
+
const walk = path => {
|
|
28
|
+
let pending = path.slice(1).split("/");
|
|
29
|
+
let current = "/";
|
|
30
|
+
let expansions = 0;
|
|
31
|
+
while (pending.length) {
|
|
32
|
+
const [component, ...remaining] = pending;
|
|
33
|
+
pending = remaining;
|
|
34
|
+
if (!component || component === ".") continue;
|
|
35
|
+
if (component === "..") {
|
|
36
|
+
current = posix.dirname(current);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const entry = posix.join(current, component);
|
|
40
|
+
const info = lstat(entry);
|
|
41
|
+
if (!Number.isSafeInteger(info.uid) || (info.uid !== 0 && info.uid !== uid)) return null;
|
|
42
|
+
if (info.isSymbolicLink()) {
|
|
43
|
+
if (++expansions > MAX_SYMLINK_EXPANSIONS) return null;
|
|
44
|
+
const target = readlink(entry);
|
|
45
|
+
pending = target.startsWith("/")
|
|
46
|
+
? [...target.slice(1).split("/"), ...pending]
|
|
47
|
+
: [
|
|
48
|
+
...(current === "/" ? [] : current.slice(1).split("/")),
|
|
49
|
+
...target.split("/"),
|
|
50
|
+
...pending,
|
|
51
|
+
];
|
|
52
|
+
current = "/";
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (remaining.length || pending.length) {
|
|
56
|
+
// No writable-ancestor exemption: a shared sticky directory passes its own
|
|
57
|
+
// ownership check but lets another user replace a child entry between the
|
|
58
|
+
// ancestry walk and the exec probe, substituting an unvalidated binary.
|
|
59
|
+
if (!info.isDirectory() || (info.mode & 0o022) !== 0) return null;
|
|
60
|
+
} else if (!info.isFile() || (info.mode & 0o022) !== 0) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
current = entry;
|
|
64
|
+
}
|
|
65
|
+
return current;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const resolved = walk(candidate);
|
|
69
|
+
if (!resolved || (io.realpath ?? realpathSync.native)(resolved) !== canonical) return false;
|
|
70
|
+
const parent = lstat(posix.dirname(canonical));
|
|
71
|
+
return parent.isDirectory()
|
|
72
|
+
&& Number.isSafeInteger(parent.uid)
|
|
73
|
+
&& (parent.uid === 0 || parent.uid === uid)
|
|
74
|
+
&& (parent.mode & 0o022) === 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
20
77
|
/** Discover the first absolute PATH Bun passing version-policy and identity checks. */
|
|
21
78
|
export function findPathBun({
|
|
22
79
|
env = process.env, platform = process.platform, pinnedVersion,
|
|
@@ -54,14 +111,16 @@ export function findPathBun({
|
|
|
54
111
|
if (now() >= end) break;
|
|
55
112
|
if (!directory || !absolute(directory)) continue;
|
|
56
113
|
try {
|
|
57
|
-
const
|
|
114
|
+
const candidatePath = paths.join(directory, platform === "win32" ? "bun.exe" : "bun");
|
|
115
|
+
const path = realpath(candidatePath);
|
|
58
116
|
if (!absolute(path)) continue;
|
|
117
|
+
if (platform !== "win32" && !trustedPosixCandidate(candidatePath, path, io)) continue;
|
|
59
118
|
const identity = platform === "win32" ? path.toLowerCase() : path;
|
|
60
119
|
if (seen.has(identity)) continue;
|
|
61
120
|
seen.add(identity);
|
|
62
121
|
const file = stat(path);
|
|
63
122
|
if (!file.isFile()) continue;
|
|
64
|
-
if (platform !== "win32" && (
|
|
123
|
+
if (platform !== "win32" && (file.mode & 0o022)) continue;
|
|
65
124
|
access(path, constants.X_OK);
|
|
66
125
|
if (!validate(path)) continue;
|
|
67
126
|
const version = probe(path, ["--version"]);
|
|
@@ -36,6 +36,10 @@ export interface CodexAppServerStateResponse {
|
|
|
36
36
|
* The desktop half of a restart. Scalar-only for the same reason as the rest of this
|
|
37
37
|
* contract: pid lists and a closed-vocabulary reason, never a command line or an OS
|
|
38
38
|
* error message, both of which routinely embed a path or a username.
|
|
39
|
+
*
|
|
40
|
+
* `reason: "picker_current"` means the shell was deliberately left running because it
|
|
41
|
+
* launched after the current catalog was written, so only the app-servers restarted and
|
|
42
|
+
* the operator's unsaved client state was kept (#4761).
|
|
39
43
|
*/
|
|
40
44
|
export interface CodexDesktopRestartSummary {
|
|
41
45
|
attempted: boolean;
|
|
@@ -43,8 +47,17 @@ export interface CodexDesktopRestartSummary {
|
|
|
43
47
|
surviving: number[];
|
|
44
48
|
relaunch: "started" | "skipped";
|
|
45
49
|
reason?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Why the desktop was or was not restarted: `stale` (the shell predates the bound
|
|
52
|
+
* catalog, or this request rebound it) and `unknown` (the evidence was unreadable) both
|
|
53
|
+
* take the full restart. Absent on a proxy older than #4761.
|
|
54
|
+
*/
|
|
55
|
+
pickerState?: CodexDesktopPickerState;
|
|
46
56
|
}
|
|
47
57
|
|
|
58
|
+
export type CodexDesktopPickerState = "current" | "stale" | "unknown" | "not_running";
|
|
59
|
+
const DESKTOP_PICKER_STATES: readonly string[] = ["current", "stale", "unknown", "not_running"];
|
|
60
|
+
|
|
48
61
|
/** POST response. All four arrays are pid lists — never command lines. */
|
|
49
62
|
export interface CodexRestartResponse {
|
|
50
63
|
success: boolean;
|
|
@@ -130,6 +143,8 @@ export function isCodexRestartResponse(value: unknown): value is CodexRestartRes
|
|
|
130
143
|
if (!isPidList(d.stopped) || !isPidList(d.surviving)) return false;
|
|
131
144
|
if (d.relaunch !== "started" && d.relaunch !== "skipped") return false;
|
|
132
145
|
if (d.reason !== undefined && typeof d.reason !== "string") return false;
|
|
146
|
+
if (d.pickerState !== undefined
|
|
147
|
+
&& (typeof d.pickerState !== "string" || !DESKTOP_PICKER_STATES.includes(d.pickerState))) return false;
|
|
133
148
|
// A started relaunch cannot have left anything behind: the ladder refuses to
|
|
134
149
|
// relaunch beside a survivor precisely so a second shell never appears.
|
|
135
150
|
if (d.relaunch === "started" && (d.surviving as number[]).length > 0) return false;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { configuredOutboundFetch, effectiveProxyFor, noProxyMatches, type ProxyEnvMap, type ProxyCapableRequestInit } from "./proxy-env";
|
|
2
|
+
|
|
3
|
+
/** Bind both native desktop transports to one explicit route; never fall back after failure. */
|
|
4
|
+
export function desktopProxyFor(url: URL, env: ProxyEnvMap = process.env): string | false {
|
|
5
|
+
if (noProxyMatches(url, env)) return false;
|
|
6
|
+
const key = url.protocol === "https:" ? "HTTPS_PROXY" : "HTTP_PROXY";
|
|
7
|
+
let selected = effectiveProxyFor(url, env);
|
|
8
|
+
// Unlike an implicit Bun route, this transport explicitly binds HTTP(S) CONNECT.
|
|
9
|
+
// A malformed scheme-specific setting still refuses instead of falling through.
|
|
10
|
+
if (!selected && url.protocol === "https:" && !(env[key]?.trim() || env[key.toLowerCase()]?.trim())) {
|
|
11
|
+
const all = env.ALL_PROXY?.trim() || env.all_proxy?.trim();
|
|
12
|
+
if (all) {
|
|
13
|
+
let parsed: URL; try { parsed = new URL(all); } catch { throw new Error("desktop_egress_proxy_invalid"); }
|
|
14
|
+
if (["http:", "https:"].includes(parsed.protocol)) selected = all;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (selected) {
|
|
18
|
+
const proxy = new URL(selected);
|
|
19
|
+
if (!["http:", "https:", "socks5:", "socks5h:"].includes(proxy.protocol) || !proxy.hostname
|
|
20
|
+
|| proxy.search || proxy.hash || proxy.pathname !== "" && proxy.pathname !== "/") throw new Error("desktop_egress_proxy_invalid");
|
|
21
|
+
return selected;
|
|
22
|
+
}
|
|
23
|
+
// An explicit but unsupported route must not quietly become direct desktop traffic.
|
|
24
|
+
if ([env[key], env[key.toLowerCase()], env.ALL_PROXY, env.all_proxy].some(value => value?.trim())) throw new Error("desktop_egress_proxy_invalid");
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
export function desktopOutboundFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
|
|
28
|
+
const url = new URL(input instanceof Request ? input.url : String(input));
|
|
29
|
+
return configuredOutboundFetch(input, { ...init, proxy: desktopProxyFor(url) } as ProxyCapableRequestInit);
|
|
30
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { connect as tcpConnect, type Socket } from "node:net";
|
|
2
|
+
import { connect as tlsConnect, type TLSSocket } from "node:tls";
|
|
3
|
+
import { desktopProxyFor } from "./desktop-proxy-route";
|
|
4
|
+
import { dialTunnel, type OpaqueProxyRoute } from "./opaque-proxy-tunnel";
|
|
5
|
+
|
|
6
|
+
// The shared opaque dialer owns proxy framing; this caller owns fixed-target
|
|
7
|
+
// TLS and bounds the entire connection, including the target TLS handshake.
|
|
8
|
+
export interface DesktopTunnelOptions {
|
|
9
|
+
proxy?: string | false;
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
/** Isolated test peers; production always uses verified TLS to chatgpt.com:443. */
|
|
13
|
+
target?: { host: string; port: number };
|
|
14
|
+
ca?: string;
|
|
15
|
+
proxyCa?: string;
|
|
16
|
+
}
|
|
17
|
+
const fail = () => new Error("desktop_upstream_connection_failed");
|
|
18
|
+
function waitConnected(socket: Socket, event: "connect" | "secureConnect", signal: AbortSignal): Promise<void> {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const dispose = () => { socket.off(event, done); socket.off("error", failure); socket.off("close", failure); signal.removeEventListener("abort", failure); };
|
|
21
|
+
const done = () => { dispose(); resolve(); };
|
|
22
|
+
const failure = () => { dispose(); reject(fail()); };
|
|
23
|
+
socket.once(event, done); socket.once("error", failure); socket.once("close", failure);
|
|
24
|
+
signal.addEventListener("abort", failure, { once: true }); if (signal.aborted) failure();
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/** Fixed-destination TLS dial for native desktop upgraded sockets. No logs or direct fallback. */
|
|
28
|
+
export async function dialDesktopUpstream(options: DesktopTunnelOptions = {}): Promise<TLSSocket> {
|
|
29
|
+
const timeout = options.timeoutMs ?? 10000;
|
|
30
|
+
if (!Number.isSafeInteger(timeout) || timeout < 1 || timeout > 30000) throw fail();
|
|
31
|
+
const cancel = new AbortController(), timer = setTimeout(() => cancel.abort(), timeout);
|
|
32
|
+
const signal = options.signal ? AbortSignal.any([options.signal, cancel.signal]) : cancel.signal;
|
|
33
|
+
const sockets = new Set<Socket>();
|
|
34
|
+
const own = <T extends Socket>(socket: T): T => { sockets.add(socket); socket.on("error", () => {}); return socket; };
|
|
35
|
+
const destroy = () => { for (const socket of sockets) socket.destroy(); };
|
|
36
|
+
signal.addEventListener("abort", destroy, { once: true });
|
|
37
|
+
try {
|
|
38
|
+
if (signal.aborted) throw fail();
|
|
39
|
+
const proxy = options.proxy === undefined ? desktopProxyFor(new URL("https://chatgpt.com")) : options.proxy;
|
|
40
|
+
const target = options.target ?? { host: "chatgpt.com", port: 443 };
|
|
41
|
+
let raw: Socket;
|
|
42
|
+
if (proxy === false) {
|
|
43
|
+
raw = own(tcpConnect(target)); await waitConnected(raw, "connect", signal);
|
|
44
|
+
} else {
|
|
45
|
+
const url = new URL(proxy), protocol = url.protocol;
|
|
46
|
+
if (!["http:", "https:", "socks5:", "socks5h:"].includes(protocol) || !url.hostname || url.search || url.hash || url.pathname !== "" && url.pathname !== "/") throw fail();
|
|
47
|
+
const host = url.hostname.replace(/^\[|\]$/g, ""), port = Number(url.port) || (protocol === "https:" ? 443 : protocol.startsWith("socks") ? 1080 : 80);
|
|
48
|
+
const credentials = url.username || url.password
|
|
49
|
+
? { username: decodeURIComponent(url.username), password: decodeURIComponent(url.password) }
|
|
50
|
+
: {};
|
|
51
|
+
const endpoint = { host, port, ...credentials };
|
|
52
|
+
const route: OpaqueProxyRoute = protocol === "https:"
|
|
53
|
+
? { ...endpoint, kind: "https", proxyCa: options.proxyCa }
|
|
54
|
+
: { ...endpoint, kind: protocol.slice(0, -1) as "http" | "socks5" | "socks5h" };
|
|
55
|
+
const authority = `${target.host.includes(":") ? `[${target.host}]` : target.host}:${target.port}`;
|
|
56
|
+
// The route resolver remains authoritative; a failed proxy never becomes DIRECT.
|
|
57
|
+
raw = own(await dialTunnel(authority, route, { signal, timeoutMs: timeout, maxHeaderBytes: 65536 }));
|
|
58
|
+
}
|
|
59
|
+
if (signal.aborted || raw.destroyed) throw fail();
|
|
60
|
+
// This consumer historically closes its writable side after peer EOF. The
|
|
61
|
+
// shared opaque transport preserves half-closes for callers that need them.
|
|
62
|
+
raw.allowHalfOpen = false;
|
|
63
|
+
const secured = own(tlsConnect({ socket: raw, servername: "chatgpt.com", ca: options.ca, rejectUnauthorized: true, ALPNProtocols: ["http/1.1"] }));
|
|
64
|
+
// The CONNECT reader paused its stream before handing buffered bytes back. Bun's
|
|
65
|
+
// TLS-over-TLS path needs the outer TLS stream explicitly resumed for the new handshake.
|
|
66
|
+
raw.resume();
|
|
67
|
+
await waitConnected(secured, "secureConnect", signal);
|
|
68
|
+
if (signal.aborted) throw fail();
|
|
69
|
+
return secured;
|
|
70
|
+
} catch { destroy(); throw fail(); }
|
|
71
|
+
finally { clearTimeout(timer); signal.removeEventListener("abort", destroy); }
|
|
72
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { normalizeDeclaredToolName, type AdapterEvent } from "../types";
|
|
2
|
+
import { UNDECLARED_TOOL_CALL_ERROR_CODE } from "./errors";
|
|
3
|
+
import { releaseTranslatedEvent, type TranslatorBudget } from "./translator-budget";
|
|
4
|
+
import { releaseCompactionCiphertextLease } from "../responses/compaction";
|
|
5
|
+
|
|
6
|
+
/** Snapshot of current-turn declarations when an explicit empty catalog activates enforcement. */
|
|
7
|
+
export interface ClientToolCatalogRestriction {
|
|
8
|
+
declaredToolNames: ReadonlySet<string>;
|
|
9
|
+
bareCustomToolNames: ReadonlySet<string>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function emptyToolCatalogRefusal(
|
|
13
|
+
event: AdapterEvent,
|
|
14
|
+
catalog: ClientToolCatalogRestriction | undefined,
|
|
15
|
+
): Extract<AdapterEvent, { type: "error" }> | undefined {
|
|
16
|
+
if (!catalog) return;
|
|
17
|
+
if (event.type === "tool_call_start") {
|
|
18
|
+
const name = normalizeDeclaredToolName(event.name, catalog.declaredToolNames, undefined, catalog.bareCustomToolNames);
|
|
19
|
+
if (catalog.declaredToolNames.has(name)) return;
|
|
20
|
+
} else if (catalog.declaredToolNames.size !== 0
|
|
21
|
+
|| (event.type !== "tool_call_delta" && event.type !== "tool_call_end")) return;
|
|
22
|
+
return {
|
|
23
|
+
type: "error", status: 502, errorType: "upstream_error", code: UNDECLARED_TOOL_CALL_ERROR_CODE,
|
|
24
|
+
message: "routed provider emitted a client tool call outside the current request's explicit tool catalog",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Runs after hosted tool loops, before client delivery and continuation-state folding. */
|
|
29
|
+
export async function* guardEmptyToolCatalogStream(
|
|
30
|
+
events: AsyncIterable<AdapterEvent>,
|
|
31
|
+
catalog: ClientToolCatalogRestriction | undefined,
|
|
32
|
+
budget: TranslatorBudget,
|
|
33
|
+
): AsyncGenerator<AdapterEvent> {
|
|
34
|
+
for await (const event of events) {
|
|
35
|
+
const refusal = emptyToolCatalogRefusal(event, catalog);
|
|
36
|
+
if (refusal) {
|
|
37
|
+
releaseTranslatedEvent(event, budget);
|
|
38
|
+
releaseCompactionCiphertextLease(event, budget);
|
|
39
|
+
}
|
|
40
|
+
yield refusal ?? event;
|
|
41
|
+
if (refusal) return;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function guardEmptyToolCatalogEvents(
|
|
46
|
+
events: AdapterEvent[],
|
|
47
|
+
catalog: ClientToolCatalogRestriction | undefined,
|
|
48
|
+
budget: TranslatorBudget,
|
|
49
|
+
): AdapterEvent[] {
|
|
50
|
+
if (!catalog) return events;
|
|
51
|
+
const index = events.findIndex(event => emptyToolCatalogRefusal(event, catalog) !== undefined);
|
|
52
|
+
if (index < 0) return events;
|
|
53
|
+
const refusal = emptyToolCatalogRefusal(events[index]!, catalog)!;
|
|
54
|
+
for (const event of events.slice(index)) {
|
|
55
|
+
if ((event.type === "done" || event.type === "incomplete" || event.type === "error") && event.usage) {
|
|
56
|
+
refusal.usage = event.usage;
|
|
57
|
+
}
|
|
58
|
+
releaseTranslatedEvent(event, budget);
|
|
59
|
+
releaseCompactionCiphertextLease(event, budget);
|
|
60
|
+
}
|
|
61
|
+
return [...events.slice(0, index), refusal];
|
|
62
|
+
}
|
package/src/lib/errors.ts
CHANGED
|
@@ -18,6 +18,13 @@ export const ENCRYPTED_FUNCTION_OUTPUT_REJECTION =
|
|
|
18
18
|
*/
|
|
19
19
|
export const SEND_BUDGET_EXHAUSTED_CODE = "request_send_budget_exhausted";
|
|
20
20
|
export const SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE = "spend_ledger_storage_unavailable";
|
|
21
|
+
/**
|
|
22
|
+
* Wire code for the undeclared-tool guard's deterministic refusal. Endpoint translators
|
|
23
|
+
* read it to keep the refusal non-retryable instead of folding it into the transient
|
|
24
|
+
* upstream-error class. Declared here rather than in the guard module because the shared
|
|
25
|
+
* envelope formatter decides whether the identity survives serialization.
|
|
26
|
+
*/
|
|
27
|
+
export const UNDECLARED_TOOL_CALL_ERROR_CODE = "undeclared_tool_call";
|
|
21
28
|
|
|
22
29
|
/** First nonblank string across the canonical upstream error paths, in priority order. */
|
|
23
30
|
export function upstreamErrorMessageFromPayload(payload: unknown): string | undefined {
|
|
@@ -643,10 +650,22 @@ export function adapterFailureFromMessage(message: string): { httpStatus: number
|
|
|
643
650
|
return { httpStatus, error };
|
|
644
651
|
}
|
|
645
652
|
|
|
653
|
+
/** A declared client refusal: retrying the identical request cannot change the verdict. */
|
|
654
|
+
const CLIENT_ERROR_STATUS: Record<string, 400 | 401 | 403 | 404> = {
|
|
655
|
+
invalid_request_error: 400,
|
|
656
|
+
authentication_error: 401,
|
|
657
|
+
permission_error: 403,
|
|
658
|
+
not_found_error: 404,
|
|
659
|
+
};
|
|
660
|
+
|
|
646
661
|
/** Narrow structured classes carried through a synthesized bare-error terminal. */
|
|
647
662
|
export function recognizedUpstreamError(error: { type?: unknown; code?: unknown }):
|
|
648
|
-
{ type: string; code: string; httpStatus: 429 | 503 } | undefined {
|
|
663
|
+
{ type: string; code: string; httpStatus: 400 | 401 | 403 | 404 | 429 | 503 } | undefined {
|
|
649
664
|
const code = typeof error.code === "string" ? error.code : undefined;
|
|
665
|
+
const clientClass = code ?? (typeof error.type === "string" ? error.type : undefined);
|
|
666
|
+
if (clientClass !== undefined && Object.hasOwn(CLIENT_ERROR_STATUS, clientClass)) {
|
|
667
|
+
return { type: clientClass, code: clientClass, httpStatus: CLIENT_ERROR_STATUS[clientClass] };
|
|
668
|
+
}
|
|
650
669
|
// An explicit unknown code must not gain a recognized class from its type or copy.
|
|
651
670
|
if (code !== undefined && !["rate_limit_exceeded", "rate_limit_error", "server_is_overloaded", "overloaded_error"].includes(code)) return undefined;
|
|
652
671
|
if (code === "rate_limit_exceeded" || code === "rate_limit_error"
|
|
@@ -673,8 +692,13 @@ export function httpStatusFromTerminalError(error: {
|
|
|
673
692
|
if (isCyberPolicyCode(error.code) || (error.message ? isCyberPolicyMessage(error.message) : false)) {
|
|
674
693
|
return 400;
|
|
675
694
|
}
|
|
695
|
+
const message = error.message ?? "";
|
|
676
696
|
const recognized = recognizedUpstreamError(error);
|
|
677
|
-
|
|
697
|
+
// Client-closed messages often arrive as invalid_request_error after classifyError, so a
|
|
698
|
+
// recognized 400 yields to the client-closed check below; every other class keeps its order.
|
|
699
|
+
if (recognized && !(recognized.httpStatus === 400 && message && isClientClosedMessage(message))) {
|
|
700
|
+
return recognized.httpStatus;
|
|
701
|
+
}
|
|
678
702
|
if (error.type === "rate_limit_error" || error.code === "rate_limit_exceeded") return 429;
|
|
679
703
|
if (error.type === "authentication_error" || error.code === "invalid_api_key") return 401;
|
|
680
704
|
if (
|
|
@@ -686,7 +710,6 @@ export function httpStatusFromTerminalError(error: {
|
|
|
686
710
|
if (error.type === "server_error" && error.code === "server_is_overloaded") return 503;
|
|
687
711
|
// Client-closed messages often arrive as invalid_request_error after classifyError; check message
|
|
688
712
|
// before treating every invalid_request_error as HTTP 400.
|
|
689
|
-
const message = error.message ?? "";
|
|
690
713
|
if (message && isClientClosedMessage(message)) return 499;
|
|
691
714
|
if (error.type === "invalid_request_error") return 400;
|
|
692
715
|
if (error.type === "proxy_error") return 500;
|
package/src/lib/gcp-adc.ts
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
* Application Default Credentials (ADC) resolution for Vertex AI.
|
|
3
3
|
*
|
|
4
4
|
* Direct WebCrypto + REST implementation (no `google-auth-library`). Sources, in priority order:
|
|
5
|
+
* 0. A `gcp-sa:<account>` reference (the provider's active key resolving to one) → the Service
|
|
6
|
+
* Account / authorized_user JSON stored in the OS credential store under that account
|
|
7
|
+
* (`keychain:<account>` secret, written by the key-store save path when the user pastes a
|
|
8
|
+
* credential JSON directly into the API-key slot). See resolveGcpCredentialMarker.
|
|
5
9
|
* 1. `GOOGLE_APPLICATION_CREDENTIALS` env → file with `type: "service_account"` (RS256 JWT
|
|
6
10
|
* exchange) or `type: "authorized_user"` (refresh-token exchange).
|
|
7
11
|
* 2. gcloud user ADC (`CLOUDSDK_CONFIG`, else `%APPDATA%\gcloud` on Windows, else
|
|
@@ -18,6 +22,8 @@ import { Buffer } from "node:buffer";
|
|
|
18
22
|
import * as os from "node:os";
|
|
19
23
|
import * as path from "node:path";
|
|
20
24
|
import { readFileSync, existsSync, statSync } from "node:fs";
|
|
25
|
+
import { GCP_CREDENTIAL_MARKER_PREFIX, providerKeychainEntry } from "../providers/api-key-resolve";
|
|
26
|
+
export { isGcpCredentialMaterial } from "./gcp-credential-input";
|
|
21
27
|
import {
|
|
22
28
|
captureConfigGeneration,
|
|
23
29
|
sweepExpiredOnWrite,
|
|
@@ -36,6 +42,50 @@ const TOKEN_TIMEOUT_MS = 15_000;
|
|
|
36
42
|
const TOKEN_ATTEMPTS = 3;
|
|
37
43
|
const TOKEN_RETRY_BASE_MS = 300;
|
|
38
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Reference prefix stored in `provider.apiKey` (and `apiKeyPool` entries) when the user pastes a
|
|
47
|
+
* GCP credential JSON directly into an API-key slot. The secret JSON lives in the OS credential
|
|
48
|
+
* store under the same service the `keychain:` references use; only the marker is persisted to
|
|
49
|
+
* `config.json`. The leaf reference prefix also identifies already-stored material for migration.
|
|
50
|
+
*/
|
|
51
|
+
export { GCP_CREDENTIAL_MARKER_PREFIX } from "../providers/api-key-resolve";
|
|
52
|
+
|
|
53
|
+
/** Extract a marker account; malformed explicit markers never opt into host ADC. */
|
|
54
|
+
export function gcpCredentialMarkerAccount(value: string | undefined): string | undefined {
|
|
55
|
+
if (typeof value !== "string" || !value.startsWith(GCP_CREDENTIAL_MARKER_PREFIX)) return undefined;
|
|
56
|
+
const account = value.slice(GCP_CREDENTIAL_MARKER_PREFIX.length);
|
|
57
|
+
if (!account.trim()) throw new Error("GCP credential marker requires a non-empty account");
|
|
58
|
+
return account;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Whether a pasted API-key value is in fact a GCP credential JSON. Used by the save path to
|
|
63
|
+
* divert JSON material out of the literal-key flow before it reaches `config.json`. Deliberately
|
|
64
|
+
* strict: only a `type` field with a known ADC credential type counts, so a user's literal API
|
|
65
|
+
* key that happens to start with `{` is never eaten.
|
|
66
|
+
*/
|
|
67
|
+
export function parseGcpCredentialJson(value: string): AdcFileCredentials | undefined {
|
|
68
|
+
const trimmed = (value ?? "").trim();
|
|
69
|
+
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return undefined;
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
|
|
72
|
+
if (parsed?.type === "service_account") {
|
|
73
|
+
if (typeof parsed.client_email !== "string" || !parsed.client_email.trim()) return undefined;
|
|
74
|
+
if (typeof parsed.private_key !== "string" || !parsed.private_key.trim()) return undefined;
|
|
75
|
+
return parsed as unknown as AdcFileCredentials;
|
|
76
|
+
}
|
|
77
|
+
if (parsed?.type === "authorized_user") {
|
|
78
|
+
if (typeof parsed.client_id !== "string" || !parsed.client_id.trim()) return undefined;
|
|
79
|
+
if (typeof parsed.client_secret !== "string" || !parsed.client_secret.trim()) return undefined;
|
|
80
|
+
if (typeof parsed.refresh_token !== "string" || !parsed.refresh_token.trim()) return undefined;
|
|
81
|
+
return parsed as unknown as AdcFileCredentials;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
} catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
39
89
|
interface CachedToken {
|
|
40
90
|
token: string;
|
|
41
91
|
expiresAtMs: number;
|
|
@@ -81,7 +131,10 @@ export function sweepExpiredGcpAdcTokens(now = Date.now()): number {
|
|
|
81
131
|
|
|
82
132
|
export function reconcileGcpAdcTokens(context: GenerationContext): number {
|
|
83
133
|
if (context.generation <= lastReconciledGeneration) return 0;
|
|
84
|
-
|
|
134
|
+
// The periodic sweep registration has no request context, so no marker is active here: markers
|
|
135
|
+
// travel per-request via getVertexAccessToken(options.markerAccount), and a marker-token cache
|
|
136
|
+
// entry whose account no longer resolves is simply pruned by this sweep when it expires.
|
|
137
|
+
const currentSource = currentAdcSourceKey(undefined);
|
|
85
138
|
let removed = 0;
|
|
86
139
|
for (const source of tokenCache.keys()) {
|
|
87
140
|
if (source === currentSource) continue;
|
|
@@ -131,7 +184,29 @@ function readJsonFile<T>(filePath: string): T | undefined {
|
|
|
131
184
|
return JSON.parse(readFileSync(filePath, "utf8")) as T;
|
|
132
185
|
}
|
|
133
186
|
|
|
134
|
-
|
|
187
|
+
/**
|
|
188
|
+
* Read the credential JSON for a `gcp-sa:<account>` marker from the OS credential store. Uses the
|
|
189
|
+
* same service name and the same injectable entry factory as the `keychain:` API-key references
|
|
190
|
+
* (api-key-resolve.providerKeychainEntry), so one probe surface and one test seam cover both.
|
|
191
|
+
* Mirrors `readKeychain`'s fail-closed policy: an unreadable entry yields no credential (the
|
|
192
|
+
* caller surfaces the failure instead of silently falling through to the next ADC source).
|
|
193
|
+
*/
|
|
194
|
+
function readMarkerCredential(account: string): AdcFileCredentials | undefined {
|
|
195
|
+
try {
|
|
196
|
+
const raw = providerKeychainEntry(account).getPassword();
|
|
197
|
+
if (typeof raw !== "string" || !raw.trim()) return undefined;
|
|
198
|
+
return parseGcpCredentialJson(raw);
|
|
199
|
+
} catch {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function loadAdcCredentials(markerAccount: string | undefined): { source: string; creds: AdcFileCredentials } | undefined {
|
|
205
|
+
if (markerAccount) {
|
|
206
|
+
const creds = readMarkerCredential(markerAccount);
|
|
207
|
+
if (!creds) throw new Error("GCP credential marker is set but its credential could not be read from the OS credential store");
|
|
208
|
+
return { source: `marker:${markerAccount}`, creds };
|
|
209
|
+
}
|
|
135
210
|
const gacPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
|
136
211
|
if (gacPath) {
|
|
137
212
|
const creds = readJsonFile<AdcFileCredentials>(gacPath);
|
|
@@ -148,9 +223,10 @@ function loadAdcCredentials(): { source: string; creds: AdcFileCredentials } | u
|
|
|
148
223
|
* The cache key for the source the NEXT resolve would use, computed cheaply (no network). Lets the
|
|
149
224
|
* cache return a token only when it still matches the active credential source, so an in-process
|
|
150
225
|
* change to GOOGLE_APPLICATION_CREDENTIALS (or the user ADC file) does not keep serving a stale
|
|
151
|
-
* token from a different source. Falls back to "metadata" when no file/env ADC is present.
|
|
226
|
+
* token from a different source. Falls back to "metadata" when no marker/file/env ADC is present.
|
|
152
227
|
*/
|
|
153
|
-
function currentAdcSourceKey(): string {
|
|
228
|
+
function currentAdcSourceKey(markerAccount: string | undefined): string {
|
|
229
|
+
if (markerAccount) return `marker:${markerAccount}`;
|
|
154
230
|
const gacPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
|
155
231
|
if (gacPath) return fileSourceTag("gac", gacPath);
|
|
156
232
|
const userPath = userAdcPath();
|
|
@@ -279,8 +355,8 @@ async function fetchMetadataToken(signal: AbortSignal | undefined, fetchImpl: Fe
|
|
|
279
355
|
}
|
|
280
356
|
}
|
|
281
357
|
|
|
282
|
-
async function resolveAccessTokenUncached(signal: AbortSignal | undefined, fetchImpl: FetchImpl): Promise<{ source: string; token: TokenResponse }> {
|
|
283
|
-
const adc = loadAdcCredentials();
|
|
358
|
+
async function resolveAccessTokenUncached(markerAccount: string | undefined, signal: AbortSignal | undefined, fetchImpl: FetchImpl): Promise<{ source: string; token: TokenResponse }> {
|
|
359
|
+
const adc = loadAdcCredentials(markerAccount);
|
|
284
360
|
if (adc) {
|
|
285
361
|
const token = adc.creds.type === "service_account"
|
|
286
362
|
? await exchangeJwtForToken(adc.creds, signal, fetchImpl)
|
|
@@ -295,14 +371,25 @@ async function resolveAccessTokenUncached(signal: AbortSignal | undefined, fetch
|
|
|
295
371
|
}
|
|
296
372
|
|
|
297
373
|
/** Returns a Bearer access token for the `Authorization` header on Vertex AI calls (cached + refreshed). */
|
|
298
|
-
export async function getVertexAccessToken(options?: { signal?: AbortSignal; fetch?: FetchImpl }): Promise<string> {
|
|
374
|
+
export async function getVertexAccessToken(options?: { signal?: AbortSignal; fetch?: FetchImpl; markerAccount?: string }): Promise<string> {
|
|
375
|
+
// The marker travels as an argument, never as process-global state: a module-level "active
|
|
376
|
+
// marker" written by the router would race across concurrent requests for different providers
|
|
377
|
+
// and could resolve one provider's Vertex call with another provider's service account. The
|
|
378
|
+
// adapter passes `gcpCredentialMarkerAccount(provider.apiKey)` per request instead.
|
|
379
|
+
const markerAccount = options?.markerAccount;
|
|
299
380
|
const fetchImpl = options?.fetch ?? globalThis.fetch.bind(globalThis);
|
|
300
381
|
const skew = getRefreshSkewMs();
|
|
301
382
|
const now = Date.now();
|
|
302
383
|
|
|
303
384
|
// Only serve a cached token that matches the source the next resolve would actually use; prune
|
|
304
385
|
// expired or now-stale (different-source) entries so a credential-source change is honored.
|
|
305
|
-
const expectedSource = currentAdcSourceKey();
|
|
386
|
+
const expectedSource = currentAdcSourceKey(markerAccount);
|
|
387
|
+
// A cached bearer token is usable only while the explicitly selected store entry remains
|
|
388
|
+
// readable. Removal or a locked store must fail closed before the cache can return it.
|
|
389
|
+
if (markerAccount && !readMarkerCredential(markerAccount)) {
|
|
390
|
+
tokenCache.delete(expectedSource);
|
|
391
|
+
throw new Error("GCP credential marker is set but its credential could not be read from the OS credential store");
|
|
392
|
+
}
|
|
306
393
|
const writerGeneration = captureConfigGeneration();
|
|
307
394
|
for (const [source, cached] of tokenCache) {
|
|
308
395
|
if (source === expectedSource && cached.expiresAtMs - skew > now) return cached.token;
|
|
@@ -317,7 +404,7 @@ export async function getVertexAccessToken(options?: { signal?: AbortSignal; fet
|
|
|
317
404
|
|
|
318
405
|
const promise = (async () => {
|
|
319
406
|
try {
|
|
320
|
-
const { source, token } = await resolveAccessTokenUncached(options?.signal, fetchImpl);
|
|
407
|
+
const { source, token } = await resolveAccessTokenUncached(markerAccount, options?.signal, fetchImpl);
|
|
321
408
|
const expiresAtMs = Date.now() + Math.max(0, token.expires_in * 1000);
|
|
322
409
|
if (writerGeneration >= lastReconciledGeneration || liveSources.has(source)) {
|
|
323
410
|
tokenCache.set(source, { token: token.access_token, expiresAtMs });
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Structured JSON is never literal key material; malformed GCP text is also refused. */
|
|
2
|
+
export function isGcpCredentialMaterial(value: string): boolean {
|
|
3
|
+
const credentialField = /\b(?:service_account|authorized_user|external_account|private_key|refresh_token|client_secret)\b|-----BEGIN [A-Z ]*PRIVATE KEY-----/;
|
|
4
|
+
if (/[\[\{"']/.test(value) && credentialField.test(value)) return true;
|
|
5
|
+
// Reject structured carriers outright rather than recursively guessing their credential shape.
|
|
6
|
+
// This includes quoted/array/object wrappers with arbitrarily escaped or nested JSON inside.
|
|
7
|
+
try {
|
|
8
|
+
const parsed: unknown = JSON.parse(value);
|
|
9
|
+
return typeof parsed === "string" || (parsed !== null && typeof parsed === "object");
|
|
10
|
+
}
|
|
11
|
+
catch { return false; }
|
|
12
|
+
}
|
|
@@ -72,6 +72,11 @@ export async function buildLabProviderAuthHeaders(
|
|
|
72
72
|
&& configuredAnthropicInstance(config, routeContext.providerId) !== routeContext.providerId) {
|
|
73
73
|
throw new TransportError("auth_blocked", "anthropic instance unavailable");
|
|
74
74
|
}
|
|
75
|
+
// The DSH account token is sent only by its own adapter to its pinned endpoint; the generic
|
|
76
|
+
// probe transport would use another endpoint and auth scheme.
|
|
77
|
+
if (routeContext.providerId === "dsh-account") {
|
|
78
|
+
throw new TransportError("auth_blocked", "dsh-account probes are not supported");
|
|
79
|
+
}
|
|
75
80
|
try {
|
|
76
81
|
const snapshot = await getValidAccessTokenSnapshot(routeContext.providerId);
|
|
77
82
|
headers.Authorization = `Bearer ${snapshot.accessToken}`;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-local internal-call capability.
|
|
3
|
+
*
|
|
4
|
+
* Some requests ARE the proxy itself talking to its own data plane: the advisor sidecar's
|
|
5
|
+
* loopback consultation is the first user. Such a request must be recognized as INTERNAL without
|
|
6
|
+
* trusting anything the client controls. A client-supplied header value is not evidence — any
|
|
7
|
+
* external caller can send it — and neither is the peer address: a public listener reached
|
|
8
|
+
* through Docker, WSL, a tunnel, or port forwarding can present as loopback on the last hop, as
|
|
9
|
+
* the local-management attestation notes.
|
|
10
|
+
*
|
|
11
|
+
* The authority is therefore process-owned: a 256-bit random value minted once per process, kept
|
|
12
|
+
* in memory only. It is never written to config or disk, never logged, never returned by the
|
|
13
|
+
* management API, never placed in usage or request metadata, and never forwarded upstream. A new
|
|
14
|
+
* process mints a new value, so a token captured from an older process is worthless. Comparison
|
|
15
|
+
* is timing-safe and shape-checked, reusing the same secret shape as the local attestation
|
|
16
|
+
* module.
|
|
17
|
+
*
|
|
18
|
+
* The same primitive is what the vision-describe fence would need for the same reason; wiring
|
|
19
|
+
* that surface is deliberately left out of this change (recorded as a pre-existing analogous
|
|
20
|
+
* issue) so the change stays scoped.
|
|
21
|
+
*/
|
|
22
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
23
|
+
import { isLocalAttestationSecret } from "./local-management-attestation";
|
|
24
|
+
|
|
25
|
+
/** Header an internal sidecar presents on its own loopback request. */
|
|
26
|
+
export const ADVISOR_INTERNAL_CAPABILITY_HEADER = "x-opencodex-advisor-internal";
|
|
27
|
+
|
|
28
|
+
let processCapability: string | null = null;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The current process's internal-call capability. Minted lazily on first use (one random draw,
|
|
32
|
+
* no I/O), so an install that never enables the advisor never generates one.
|
|
33
|
+
*/
|
|
34
|
+
export function internalCallCapability(): string {
|
|
35
|
+
if (processCapability === null) processCapability = randomBytes(32).toString("base64url");
|
|
36
|
+
return processCapability;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Test seam: install a deterministic value, or `null` to force a fresh mint. */
|
|
40
|
+
export function setInternalCallCapabilityForTests(value: string | null): void {
|
|
41
|
+
processCapability = value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* True only when the supplied header value IS this process's capability. Shape-checked first so
|
|
46
|
+
* a malformed value cannot reach the comparison, then compared in constant time.
|
|
47
|
+
*/
|
|
48
|
+
export function isInternalCallCapability(supplied: string | null | undefined): boolean {
|
|
49
|
+
if (typeof supplied !== "string" || !isLocalAttestationSecret(supplied)) return false;
|
|
50
|
+
if (processCapability === null) return false;
|
|
51
|
+
const expected = processCapability;
|
|
52
|
+
const suppliedBytes = Buffer.from(supplied);
|
|
53
|
+
const expectedBytes = Buffer.from(expected);
|
|
54
|
+
return suppliedBytes.length === expectedBytes.length && timingSafeEqual(suppliedBytes, expectedBytes);
|
|
55
|
+
}
|