@bitkyc08/opencodex 2.59.0 → 2.61.0-preview.20260922
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/AGENTS_INSTALL.md +64 -0
- package/README.md +28 -1
- package/bin/ocx.mjs +382 -209
- package/gui/dist/assets/App-E64Rzjap.js +50 -0
- package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
- package/gui/dist/assets/index-DpdfZWMK.js +86 -0
- package/gui/dist/assets/index-_bpvxJu0.css +1 -0
- package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/stepfun-color.svg +1 -0
- package/package.json +8 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +20 -1
- package/src/adapters/base.ts +23 -0
- package/src/adapters/coding-agent/protocol.ts +36 -6
- package/src/adapters/coding-agent/turn.ts +32 -4
- package/src/adapters/command-code.ts +52 -4
- package/src/adapters/cursor/catalog.ts +51 -7
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/protobuf-request.ts +6 -3
- package/src/adapters/cursor/request-builder.ts +20 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +14 -3
- package/src/adapters/declaration-carrier.ts +45 -0
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
- package/src/adapters/devin.ts +125 -36
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +34 -7
- package/src/adapters/google-errors.ts +7 -1
- package/src/adapters/google-http.ts +49 -10
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +66 -11
- package/src/adapters/image.ts +4 -1
- package/src/adapters/input-media-guard.ts +21 -9
- package/src/adapters/kiro/usage.ts +3 -2
- package/src/adapters/kiro-tool-fallback.ts +1 -1
- package/src/adapters/ollama-native.ts +6 -0
- package/src/adapters/openai-chat/developer-role.ts +61 -0
- package/src/adapters/openai-chat/messages.ts +46 -27
- package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
- package/src/adapters/openai-chat/passthrough.ts +33 -9
- package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +23 -58
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +17 -3
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/registry.ts +3 -2
- package/src/adapters/run-turn-queue.ts +178 -29
- package/src/adapters/xai-web-search.ts +16 -1
- package/src/bridge/errors.ts +8 -2
- package/src/bridge/response-json.ts +9 -1
- package/src/bridge/sse.ts +13 -151
- package/src/chat/inbound.ts +141 -5
- package/src/claude/desktop-3p.ts +7 -1
- package/src/claude/desktop-first-party.ts +183 -0
- package/src/claude/desktop-gateway-state.ts +41 -0
- package/src/claude/inbound-content-options.ts +6 -0
- package/src/claude/inbound.ts +32 -6
- package/src/claude/intercept/connect-proxy.ts +179 -0
- package/src/claude/intercept/listener.ts +122 -0
- package/src/claude/intercept/local-ca.ts +298 -0
- package/src/claude/intercept/runtime.ts +98 -0
- package/src/claude/intercept/settings.ts +189 -0
- package/src/cli/access.ts +87 -0
- package/src/cli/account-auth.ts +19 -0
- package/src/cli/account-extended.ts +4 -4
- package/src/cli/capabilities.ts +31 -0
- package/src/cli/claude-desktop.ts +206 -16
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/companion.ts +56 -0
- package/src/cli/dispatch.ts +46 -7
- package/src/cli/doctor.ts +28 -9
- package/src/cli/ensure-desired-integrations.ts +43 -5
- package/src/cli/help.ts +7 -9
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +203 -59
- package/src/cli/init.ts +8 -0
- package/src/cli/integrations.ts +7 -1
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/cli/registry.ts +41 -2
- package/src/cli/resolve.ts +230 -0
- package/src/cli/root.ts +24 -1
- package/src/cli/start-ownership-publication.ts +56 -0
- package/src/cli/status-probes.ts +2 -18
- package/src/cli/status.ts +62 -0
- package/src/cli/stop-report.ts +143 -0
- package/src/cli/uninstall-plan.ts +9 -0
- package/src/client/machine-api.ts +2 -2
- package/src/client/machine-listener.ts +4 -7
- package/src/client/runtime.ts +26 -2
- package/src/clients/aside-profiles.ts +4 -0
- package/src/clients/config-export/zcode-store.ts +157 -0
- package/src/clients/config-export.ts +36 -0
- package/src/codex/account-store.ts +65 -0
- package/src/codex/app-server-processes.ts +72 -40
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/login-flow.ts +6 -1
- package/src/codex/auth-api/pool-quota-probe.ts +30 -7
- package/src/codex/autostart-health.ts +28 -0
- package/src/codex/catalog/build-entries.ts +2 -2
- package/src/codex/catalog/effort.ts +3 -3
- package/src/codex/catalog/gather-capture.ts +21 -2
- package/src/codex/catalog/model-hints.ts +29 -28
- package/src/codex/catalog/parsing.ts +7 -0
- package/src/codex/catalog/provider-models.ts +43 -17
- package/src/codex/catalog/retained-sync.ts +24 -28
- package/src/codex/catalog/routed-gather.ts +19 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +2 -2
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +31 -166
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/inject/config-toml.ts +41 -6
- package/src/codex/inject/paginated-openai-compat.ts +90 -0
- package/src/codex/inject.ts +18 -15
- package/src/codex/injected-marker.ts +18 -0
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/main-account.ts +6 -0
- package/src/codex/model-cache.ts +99 -6
- package/src/codex/model-entitlement-admission.ts +59 -0
- package/src/codex/model-entitlements.ts +116 -54
- package/src/codex/native-main-admission.ts +83 -0
- package/src/codex/observed-model-denials.ts +101 -8
- package/src/codex/prompt-text-probe.ts +9 -6
- package/src/codex/routing/health-store.ts +39 -0
- package/src/codex/routing/selection.ts +37 -1
- package/src/codex/routing.ts +12 -42
- package/src/codex/shim-templates.ts +29 -3
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +22 -4
- package/src/combos/failover.ts +3 -0
- package/src/companion/settings.ts +132 -0
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/atomic-write.ts +117 -5
- package/src/config/diagnostics.ts +22 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +52 -7
- package/src/config/process-state.ts +1 -1
- package/src/config/proxy-env.ts +8 -2
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +31 -1
- package/src/config/schema/leaf-validators.ts +59 -0
- package/src/config.ts +3 -3
- package/src/generated/compatibility-version.json +604 -280
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/config-io.ts +44 -10
- package/src/integrations/merge.ts +120 -13
- package/src/integrations/mutation-plan.ts +921 -0
- package/src/integrations/registry.ts +38 -0
- package/src/integrations/state.ts +78 -45
- package/src/integrations/target.ts +208 -0
- package/src/integrations/writer.ts +134 -110
- package/src/lab/conformance/fixture-provider.ts +5 -0
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/browser-launch-notice.ts +59 -0
- package/src/lib/bun-runtime.ts +6 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +63 -0
- package/src/lib/errors.ts +79 -0
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/open-url.ts +51 -7
- package/src/lib/package-tree-integrity.ts +2 -1
- package/src/lib/package-version.ts +8 -0
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-egress.ts +310 -0
- package/src/lib/provider-outbound.ts +109 -16
- package/src/lib/proxy-env.ts +82 -7
- package/src/lib/request-execution-budget.ts +72 -0
- package/src/lib/request-failure-attribution.ts +183 -0
- package/src/lib/request-failure-model.ts +236 -0
- package/src/lib/request-resend-gate.ts +138 -0
- package/src/lib/socks5-fetch.ts +136 -26
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +218 -27
- package/src/lib/standalone.ts +16 -0
- package/src/lib/upstream-retry.ts +167 -16
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/winsw.ts +2 -2
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +1 -0
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +27 -108
- package/src/oauth/login-cli.ts +80 -29
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/providers/api-key-resolve.ts +133 -0
- package/src/providers/api-key-selection.ts +5 -1
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/key-failover.ts +97 -19
- package/src/providers/key-store.ts +34 -110
- package/src/providers/model-rename-fields.ts +147 -0
- package/src/providers/model-rename-migration.ts +179 -38
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/vendor-probes-key.ts +38 -23
- package/src/providers/reasoning-metadata.ts +43 -18
- package/src/providers/registry/entries-core.ts +47 -20
- package/src/providers/registry/entries-extended.ts +63 -4
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +56 -10
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/providers/xai-transport.ts +12 -1
- package/src/reasoning-effort.ts +8 -0
- package/src/responses/apply-patch-envelope.ts +0 -12
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/function-call-compat.ts +38 -1
- package/src/responses/inline-document.ts +65 -0
- package/src/responses/input-media.ts +42 -8
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/muse-tool-name-alias.ts +19 -0
- package/src/responses/parser-content.ts +8 -2
- package/src/responses/parser-tools.ts +3 -0
- package/src/responses/parser.ts +3 -1
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +3 -0
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +108 -117
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/admission-model-scope.ts +219 -0
- package/src/server/audio-live.ts +9 -3
- package/src/server/audio-upstream.ts +18 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +60 -4
- package/src/server/chat-native.ts +19 -4
- package/src/server/claude-messages.ts +61 -20
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/grok-responses-snapshot-repair.ts +113 -11
- package/src/server/gui-freshness.ts +103 -0
- package/src/server/gui-static.ts +7 -9
- package/src/server/images.ts +59 -6
- package/src/server/index/claude-intercept-lifecycle.ts +49 -0
- package/src/server/index/serve-options.ts +142 -39
- package/src/server/index/spend-ledger-lifecycle.ts +92 -0
- package/src/server/index/startup-warnings.ts +24 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +34 -38
- package/src/server/lifecycle.ts +4 -4
- package/src/server/live-call-bindings.ts +6 -0
- package/src/server/live.ts +88 -3
- package/src/server/management/agent-settings-routes.ts +121 -36
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/companion-routes.ts +77 -0
- package/src/server/management/config-routes.ts +18 -2
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/logs-usage-routes.ts +19 -0
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/native-integration-routes.ts +103 -6
- package/src/server/management/oauth-account-routes.ts +45 -7
- package/src/server/management/route-registry.ts +20 -0
- package/src/server/management/shared.ts +28 -4
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management/usage-timeline-routes.ts +44 -0
- package/src/server/management-api.ts +10 -9
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +75 -0
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +138 -12
- package/src/server/request-log-failure-attribution.ts +99 -0
- package/src/server/request-log.ts +169 -2
- package/src/server/request-metrics.ts +298 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +11 -6
- package/src/server/responses/codex-ws-wire.ts +34 -8
- package/src/server/responses/combo-stream-preflight.ts +168 -6
- package/src/server/responses/compact.ts +43 -10
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +8 -3
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +6 -12
- package/src/server/responses/core-opaque-recovery.ts +91 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/fetch-helpers.ts +124 -8
- package/src/server/responses/input-admission.ts +10 -0
- package/src/server/responses/passthrough-delivery.ts +45 -15
- package/src/server/responses/passthrough-dispatch.ts +215 -44
- package/src/server/responses/passthrough-error.ts +27 -8
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +109 -18
- package/src/server/responses/request-send-budget.ts +17 -1
- package/src/server/responses/request-sidecar-auth.ts +1 -1
- package/src/server/responses/request-transport.ts +26 -6
- package/src/server/responses/reset-replay.ts +108 -0
- package/src/server/responses/run-turn-execution.ts +25 -3
- package/src/server/responses/sidecar-execution.ts +17 -2
- package/src/server/responses/ws-upstream.ts +14 -27
- package/src/server/responses-custom-tool-repair.ts +27 -54
- package/src/server/responses-request-tool-scope.ts +214 -0
- package/src/server/responses-undeclared-tool-guard.ts +35 -2
- package/src/server/search.ts +25 -1
- package/src/server/sse-payload-rewrite.ts +1 -1
- package/src/server/usage-ledger-retention.ts +73 -0
- package/src/service/cli.ts +48 -2
- package/src/service/health.ts +3 -2
- package/src/service/install-state-contract.d.mts +27 -0
- package/src/service/install-state-contract.mjs +34 -0
- package/src/service/launchd.ts +1 -1
- package/src/service/orchestration.ts +2 -4
- package/src/service/ownership-compatibility.ts +164 -0
- package/src/service/ownership-mutation-lease.d.mts +32 -0
- package/src/service/ownership-mutation-lease.mjs +211 -0
- package/src/service/repair.ts +45 -1
- package/src/service/state-lock.ts +269 -0
- package/src/service/state-record.d.mts +36 -0
- package/src/service/state-record.mjs +138 -0
- package/src/service/state.ts +582 -68
- package/src/service/windows-taskxml.ts +11 -10
- package/src/service.ts +7 -3
- package/src/tray/windows-tray.ps1 +156 -4
- package/src/types/config.ts +48 -3
- package/src/types/provider.ts +91 -0
- package/src/types/request.ts +30 -2
- package/src/types/tools.ts +33 -0
- package/src/types.ts +4 -0
- package/src/update/index.ts +207 -63
- package/src/update/job.ts +9 -5
- package/src/update/ownership-transaction.ts +47 -0
- package/src/update/restart-ownership.ts +54 -0
- package/src/update/runtime-ownership.d.mts +40 -0
- package/src/update/runtime-ownership.mjs +122 -0
- package/src/usage/attempt-delivery.ts +198 -0
- package/src/usage/cache-diagnostic.ts +305 -0
- package/src/usage/failure-fingerprint.ts +118 -0
- package/src/usage/failure-projection-cache.ts +174 -0
- package/src/usage/failure-projection.ts +174 -0
- package/src/usage/ledger-retention.ts +165 -0
- package/src/usage/log.ts +126 -79
- package/src/usage/request-outcome.ts +150 -0
- package/src/usage/retention-contract.ts +28 -0
- package/src/usage/summary.ts +2 -2
- package/src/usage/telemetry-contract.ts +237 -0
- package/src/usage/timeline.ts +236 -0
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/alpha-search.ts +21 -1
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/gui/dist/assets/index-C5IebErG.js +0 -136
- package/gui/dist/assets/index-OESInAjC.css +0 -1
|
@@ -8,13 +8,15 @@
|
|
|
8
8
|
* becomes a terminal, non-replayable response unless the operation is explicitly safe.
|
|
9
9
|
*
|
|
10
10
|
* Deliberately narrow: timeouts, aborts, ECONNREFUSED/DNS/TLS failures, and HTTP error
|
|
11
|
-
* statuses (returned as Response, never thrown) are NOT retried.
|
|
12
|
-
* out of scope
|
|
11
|
+
* statuses (returned as Response, never thrown) are NOT retried. A reset after the response
|
|
12
|
+
* head is out of scope here, because the response has already resolved by then; the Responses
|
|
13
|
+
* transport asks the same question at that stage through the shared resend gate.
|
|
13
14
|
*
|
|
14
15
|
* MUST stay a leaf module: imports nothing from server.ts or adapters (kiro-retry imports
|
|
15
16
|
* the shared abort helpers from here).
|
|
16
17
|
*/
|
|
17
18
|
import { clearableDeadline } from "./abort";
|
|
19
|
+
import { redactSecretString } from "./redact";
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Responses the origin may already be executing. RFC 9110 §9.2.2 forbids an intermediary
|
|
@@ -96,6 +98,55 @@ export function isReplayRefusalCode(code: unknown): boolean {
|
|
|
96
98
|
/** Client-facing status for {@link UPSTREAM_RESET_REPLAY_REFUSED_CODE}. */
|
|
97
99
|
export const REPLAY_REFUSED_STATUS = 429;
|
|
98
100
|
|
|
101
|
+
/**
|
|
102
|
+
* The header every surface attaches to a replay refusal, and its only accepted value.
|
|
103
|
+
*
|
|
104
|
+
* Dropping `Retry-After` is necessary and not sufficient. The Stainless-generated clients --
|
|
105
|
+
* `openai` and `anthropic` for both Python and Node, which is what most callers of this proxy
|
|
106
|
+
* actually are -- decide from a status table (408, 409, 429 and every 5xx) and compute their own
|
|
107
|
+
* backoff when no wait is named, so a 429 with no header is still resent. `x-should-retry` is
|
|
108
|
+
* the one signal each of them reads BEFORE that table, and `"false"` is the exact string they
|
|
109
|
+
* compare against.
|
|
110
|
+
*/
|
|
111
|
+
export const REPLAY_REFUSAL_NO_RETRY_HEADER = "x-should-retry";
|
|
112
|
+
export const REPLAY_REFUSAL_NO_RETRY_VALUE = "false";
|
|
113
|
+
|
|
114
|
+
/** Spreadable form for the surfaces that build their headers as an object literal. */
|
|
115
|
+
export const REPLAY_REFUSAL_CLIENT_HEADERS: Readonly<Record<string, string>> = Object.freeze({
|
|
116
|
+
[REPLAY_REFUSAL_NO_RETRY_HEADER]: REPLAY_REFUSAL_NO_RETRY_VALUE,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Apply the one client-facing retry policy a refusal carries: no wait, and no automatic resend.
|
|
121
|
+
*
|
|
122
|
+
* Kept as a single function rather than two rules each surface repeats, because the two halves
|
|
123
|
+
* are only correct together -- a surface that removed the wait but not the suppression still
|
|
124
|
+
* hands a retrying client a turn it may already have run.
|
|
125
|
+
*/
|
|
126
|
+
export function applyReplayRefusalClientHeaders(headers: Headers): void {
|
|
127
|
+
headers.delete("retry-after");
|
|
128
|
+
headers.set(REPLAY_REFUSAL_NO_RETRY_HEADER, REPLAY_REFUSAL_NO_RETRY_VALUE);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Mark a response that re-wraps a refusal as the same refusal.
|
|
133
|
+
*
|
|
134
|
+
* The verdict has to be a property of the result the surfaces pass around, because the thing it
|
|
135
|
+
* would otherwise be read from is the status, and 429 is exactly what a refusal and a real rate
|
|
136
|
+
* limit have in common. Every formatter between the helper that made the refusal and the client
|
|
137
|
+
* builds a new Response, so each of them restates the verdict rather than dropping it.
|
|
138
|
+
*/
|
|
139
|
+
export function retainReplayRefusal<T extends Response>(response: T): T {
|
|
140
|
+
markResponseNonReplayable(response);
|
|
141
|
+
markReplayRefusalResponse(response);
|
|
142
|
+
return response;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Carry the verdict from a response onto the one that replaces it. */
|
|
146
|
+
export function carryReplayRefusal<T extends Response>(source: Response, rewrapped: T): T {
|
|
147
|
+
return isReplayRefusalResponse(source) ? retainReplayRefusal(rewrapped) : rewrapped;
|
|
148
|
+
}
|
|
149
|
+
|
|
99
150
|
// 1 initial + 2 retries: the pool may hold more than one stale socket.
|
|
100
151
|
const RESET_RETRY_MAX_ATTEMPTS = 3;
|
|
101
152
|
const RESET_RETRY_BASE_DELAY_MS = 150;
|
|
@@ -421,6 +472,20 @@ export interface ResetRetryOptions {
|
|
|
421
472
|
* uncounted but UNCOUNTABLE: the callback existed on a type those call sites never reach.
|
|
422
473
|
*/
|
|
423
474
|
onSendsConsumed?: (sends: number) => void;
|
|
475
|
+
/**
|
|
476
|
+
* Spend one operator-granted replacement for a pre-header reset this helper would otherwise
|
|
477
|
+
* refuse. Absent means no operator policy, which is the fail-closed answer.
|
|
478
|
+
*
|
|
479
|
+
* A callback rather than a count, and the difference is the whole point. A count handed to
|
|
480
|
+
* each leg of a request is a count each leg holds: a rotation leg, a refresh leg and a
|
|
481
|
+
* same-target 429 leg carry the same turn, so three numbers is three replacements of one
|
|
482
|
+
* possibly-executed inference. The callback draws on ONE allowance held by the logical
|
|
483
|
+
* request, which the post-header protocol gate draws on too.
|
|
484
|
+
*
|
|
485
|
+
* It never widens the send budget. A claimed replacement still has to fit inside
|
|
486
|
+
* `attempts`, exactly like every other send this leg makes.
|
|
487
|
+
*/
|
|
488
|
+
claimAmbiguousResend?: () => boolean;
|
|
424
489
|
}
|
|
425
490
|
|
|
426
491
|
export interface TransientRetryOptions extends ResetRetryOptions {
|
|
@@ -494,6 +559,24 @@ export function applyUpstreamRecoveryInit<T extends RequestInit>(
|
|
|
494
559
|
return { ...init, headers, keepalive: false };
|
|
495
560
|
}
|
|
496
561
|
|
|
562
|
+
/**
|
|
563
|
+
* The refusal this proxy returns for an ambiguous reset it will not replace. The WeakSet
|
|
564
|
+
* markers protect in-process recovery and the code survives JSON re-wrapping, so a combo or
|
|
565
|
+
* account-recovery layer downstream cannot read it as a replayable upstream fault. The raw
|
|
566
|
+
* exception is never exposed: it can carry credentials or request data.
|
|
567
|
+
*/
|
|
568
|
+
export function replayRefusalResponse(): Response {
|
|
569
|
+
const response = new Response(JSON.stringify({ error: {
|
|
570
|
+
type: "upstream_error",
|
|
571
|
+
code: UPSTREAM_RESET_REPLAY_REFUSED_CODE,
|
|
572
|
+
message: "The upstream connection closed before a response was received. The request may already have been processed; automatic replay was stopped.",
|
|
573
|
+
} }), {
|
|
574
|
+
status: REPLAY_REFUSED_STATUS,
|
|
575
|
+
headers: { "content-type": "application/json", ...REPLAY_REFUSAL_CLIENT_HEADERS },
|
|
576
|
+
});
|
|
577
|
+
return retainReplayRefusal(response);
|
|
578
|
+
}
|
|
579
|
+
|
|
497
580
|
/**
|
|
498
581
|
* Run `doFetch` within one send budget. Connection-reset-shaped rejections are
|
|
499
582
|
* terminal by default; only an explicitly replay-safe operation receives reset retries
|
|
@@ -511,6 +594,10 @@ export async function fetchWithResetRetry(
|
|
|
511
594
|
if (attempts === 0) throw new SendBudgetExhaustedError(opts.label);
|
|
512
595
|
let lastError: unknown;
|
|
513
596
|
let sawReset = false;
|
|
597
|
+
// True once this leg has spent the request's operator allowance. From that point the leg can
|
|
598
|
+
// only settle as the refusal: a second send of a possibly-executed turn is already out, and
|
|
599
|
+
// handing the client anything it would retry compounds it.
|
|
600
|
+
let spentOperatorReplacement = false;
|
|
514
601
|
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
515
602
|
if (opts.abortSignal?.aborted) throw abortError(opts.abortSignal);
|
|
516
603
|
// Reported before the await, one physical send at a time: a send that rejects has still
|
|
@@ -522,31 +609,37 @@ export async function fetchWithResetRetry(
|
|
|
522
609
|
} catch (err) {
|
|
523
610
|
if (opts.abortSignal?.aborted) throw err;
|
|
524
611
|
if (!isConnectionResetError(err)) {
|
|
612
|
+
// Whatever ended the leg, an operator replacement already went out, so the first send
|
|
613
|
+
// may have run the turn. Settle it as the refusal instead of throwing into a caller
|
|
614
|
+
// whose transport-failure path answers with a client-retryable 502.
|
|
615
|
+
if (spentOperatorReplacement) return replayRefusalResponse();
|
|
525
616
|
// A reset that already reached the origin is credential-visible
|
|
526
617
|
// evidence: keep it attached so the terminal rejection cannot be
|
|
527
618
|
// downgraded to the pre-connection neutral class (#914 review).
|
|
528
619
|
if (sawReset) throw new UpstreamRetryEvidenceError([], err, true);
|
|
529
620
|
throw err;
|
|
530
621
|
}
|
|
531
|
-
if (opts.replaySafe
|
|
532
|
-
//
|
|
622
|
+
if (opts.replaySafe === true) {
|
|
623
|
+
// Repeating this operation cannot duplicate anything, so an exhausted budget rethrows
|
|
624
|
+
// and the caller's own error path takes over.
|
|
625
|
+
if (attempt === attempts - 1) throw err;
|
|
626
|
+
} else {
|
|
627
|
+
// The stage table refuses an ambiguous pre-header reset. The only thing that overrides
|
|
628
|
+
// it is an operator allowance, and claiming it here is what keeps the grant single --
|
|
629
|
+
// the post-header protocol gate spends the same counter for the same logical request.
|
|
630
|
+
// Return evidence rather than throwing a generic transport error: outer catches
|
|
533
631
|
// otherwise turn it into a replayable 502 and a combo/account recovery resends it.
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
code: UPSTREAM_RESET_REPLAY_REFUSED_CODE,
|
|
539
|
-
message: "The upstream connection closed before a response was received. The request may already have been processed; automatic replay was stopped.",
|
|
540
|
-
} }), { status: REPLAY_REFUSED_STATUS, headers: { "content-type": "application/json" } });
|
|
541
|
-
markResponseNonReplayable(response);
|
|
542
|
-
markReplayRefusalResponse(response);
|
|
543
|
-
return response;
|
|
632
|
+
if (attempt + 1 >= attempts || opts.claimAmbiguousResend?.() !== true) {
|
|
633
|
+
return replayRefusalResponse();
|
|
634
|
+
}
|
|
635
|
+
spentOperatorReplacement = true;
|
|
544
636
|
}
|
|
545
|
-
if (attempt === attempts - 1) throw err;
|
|
546
637
|
sawReset = true;
|
|
547
638
|
lastError = err;
|
|
548
639
|
console.warn(
|
|
549
|
-
`[upstream-retry] connection reset${opts.label ? ` (${opts.label})` : ""} —
|
|
640
|
+
`[upstream-retry] connection reset${opts.label ? ` (${opts.label})` : ""} — ${
|
|
641
|
+
spentOperatorReplacement ? "replacing" : "retrying"
|
|
642
|
+
} (${attempt + 2}/${attempts})`,
|
|
550
643
|
);
|
|
551
644
|
await sleepWithAbort(retryBackoffDelayMs(attempt, {
|
|
552
645
|
baseDelayMs: RESET_RETRY_BASE_DELAY_MS,
|
|
@@ -661,3 +754,61 @@ export async function fetchWithTransientRetry(
|
|
|
661
754
|
opts.onSendsConsumed?.(sent);
|
|
662
755
|
}
|
|
663
756
|
}
|
|
757
|
+
|
|
758
|
+
export type ProtocolSafeRefetch = (signal?: AbortSignal) => Promise<Response>;
|
|
759
|
+
|
|
760
|
+
export interface ProtocolSafeRefetchOptions extends ResetRetryOptions {
|
|
761
|
+
/** The replacement must match the response contract already selected for the client. */
|
|
762
|
+
acceptResponse?: (response: Response) => boolean;
|
|
763
|
+
/**
|
|
764
|
+
* Spend the logical request's allowance, immediately before the replacement send.
|
|
765
|
+
*
|
|
766
|
+
* Asked here and not earlier so a failure this helper would refuse on its own terms -- a
|
|
767
|
+
* non-reset error, a cancelled caller, a spent send budget -- cannot drain the one
|
|
768
|
+
* replacement a later ambiguous reset was entitled to. False refuses the replacement.
|
|
769
|
+
*/
|
|
770
|
+
authorize?: () => boolean;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Attempt ONE caller-authorized replacement of a stream that died after the response head.
|
|
775
|
+
*
|
|
776
|
+
* The caller owns the proof that nothing was observed -- it comes from protocol inspection,
|
|
777
|
+
* not from this module -- and owns the physical-send budget. What lives here is the part that
|
|
778
|
+
* is easy to get wrong: a replacement is only usable if it is a fresh, unlocked, unread body
|
|
779
|
+
* that matches the contract already promised to the client, and anything else has to be
|
|
780
|
+
* cancelled and the original failure preserved.
|
|
781
|
+
*/
|
|
782
|
+
export async function refetchAfterProtocolSafeReset(
|
|
783
|
+
doFetch: ProtocolSafeRefetch,
|
|
784
|
+
err: unknown,
|
|
785
|
+
opts: ProtocolSafeRefetchOptions = {},
|
|
786
|
+
): Promise<Response | null> {
|
|
787
|
+
if (!isConnectionResetError(err) || opts.abortSignal?.aborted || opts.attempts === 0) return null;
|
|
788
|
+
const label = opts.label
|
|
789
|
+
? " (" + redactSecretString(opts.label).replace(/[\r\n\u0000-\u001f\u007f]/g, "").slice(0, 128) + ")"
|
|
790
|
+
: "";
|
|
791
|
+
if (opts.authorize && !opts.authorize()) {
|
|
792
|
+
console.warn("[upstream-retry] post-header reset replacement refused" + label + "; preserving original stream error");
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
let replacement: Response;
|
|
796
|
+
try {
|
|
797
|
+
replacement = await doFetch(opts.abortSignal);
|
|
798
|
+
} catch {
|
|
799
|
+
console.warn("[upstream-retry] protocol-safe refetch failed" + label + "; preserving original stream error");
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
const body = replacement.body;
|
|
803
|
+
let accepted = !opts.abortSignal?.aborted && replacement.ok && body !== null
|
|
804
|
+
&& !replacement.bodyUsed && !body.locked && !isNonReplayableResponse(replacement);
|
|
805
|
+
try { if (accepted && opts.acceptResponse) accepted = opts.acceptResponse(replacement); }
|
|
806
|
+
catch { accepted = false; }
|
|
807
|
+
if (!accepted || opts.abortSignal?.aborted || body?.locked) {
|
|
808
|
+
try { void body?.cancel().catch(() => {}); } catch { /* already locked or closed */ }
|
|
809
|
+
console.warn("[upstream-retry] protocol-safe refetch rejected" + label + "; preserving original stream error");
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
console.warn("[upstream-retry] pre-output Responses reset" + label + "; using one replacement stream");
|
|
813
|
+
return replacement;
|
|
814
|
+
}
|
|
@@ -16,7 +16,7 @@ import { decodeWindowsTextBytes } from "./windows-text";
|
|
|
16
16
|
const INTERNET_SETTINGS_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings";
|
|
17
17
|
|
|
18
18
|
export type WindowsSystemProxyResult =
|
|
19
|
-
| { kind: "proxy";
|
|
19
|
+
| { kind: "proxy"; httpUrl?: string; httpsUrl?: string }
|
|
20
20
|
| { kind: "disabled" }
|
|
21
21
|
| { kind: "socks-only" }
|
|
22
22
|
| { kind: "unsupported" }
|
|
@@ -63,35 +63,40 @@ export function readWindowsProxyRegistry(): WindowsProxyRegistryValues | null {
|
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
65
|
* `ProxyServer` is either a bare `host:port` (applies to every scheme) or a semicolon list of
|
|
66
|
-
* `scheme=host:port` entries.
|
|
67
|
-
* mirrored into
|
|
66
|
+
* `scheme=host:port` entries. Bare values apply to both HTTP and HTTPS destinations; per-scheme
|
|
67
|
+
* values retain their WinINET scope. A SOCKS-only value cannot be mirrored into HTTP(S)_PROXY.
|
|
68
68
|
*/
|
|
69
|
-
export function parseWindowsProxyServer(value: string): { kind: "proxy"
|
|
69
|
+
export function parseWindowsProxyServer(value: string): Extract<WindowsSystemProxyResult, { kind: "proxy" | "socks-only" | "disabled" }> {
|
|
70
70
|
const trimmed = value.trim();
|
|
71
71
|
if (!trimmed) return { kind: "disabled" };
|
|
72
|
-
if (!trimmed.includes("="))
|
|
72
|
+
if (!trimmed.includes("=")) {
|
|
73
|
+
const url = normalize(trimmed);
|
|
74
|
+
return url ? { kind: "proxy", httpUrl: url, httpsUrl: url } : { kind: "disabled" };
|
|
75
|
+
}
|
|
73
76
|
const entries = new Map<string, string>();
|
|
74
77
|
for (const part of trimmed.split(";")) {
|
|
75
78
|
const eq = part.indexOf("=");
|
|
76
79
|
if (eq <= 0) continue;
|
|
77
80
|
entries.set(part.slice(0, eq).trim().toLowerCase(), part.slice(eq + 1).trim());
|
|
78
81
|
}
|
|
79
|
-
const
|
|
80
|
-
|
|
82
|
+
const httpUrl = normalize(entries.get("http") ?? "");
|
|
83
|
+
const httpsUrl = normalize(entries.get("https") ?? "");
|
|
84
|
+
if (httpUrl || httpsUrl) return { kind: "proxy", ...(httpUrl && { httpUrl }), ...(httpsUrl && { httpsUrl }) };
|
|
81
85
|
if (entries.has("socks")) return { kind: "socks-only" };
|
|
82
86
|
return { kind: "disabled" };
|
|
83
87
|
}
|
|
84
88
|
|
|
85
|
-
function normalize(hostPort: string):
|
|
89
|
+
function normalize(hostPort: string): string | undefined {
|
|
90
|
+
if (!hostPort) return undefined;
|
|
86
91
|
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(hostPort) ? hostPort : `http://${hostPort}`;
|
|
87
92
|
try {
|
|
88
93
|
const url = new URL(withScheme);
|
|
89
|
-
if (!url.hostname || (url.protocol !== "http:" && url.protocol !== "https:")) return
|
|
94
|
+
if (!url.hostname || (url.protocol !== "http:" && url.protocol !== "https:")) return undefined;
|
|
90
95
|
// Keep userinfo: a credentialed proxy is valid in HTTP_PROXY. Only the log strips it.
|
|
91
96
|
const auth = url.username ? `${url.username}${url.password ? `:${url.password}` : ""}@` : "";
|
|
92
|
-
return
|
|
97
|
+
return `${url.protocol}//${auth}${url.host}`;
|
|
93
98
|
} catch {
|
|
94
|
-
return
|
|
99
|
+
return undefined;
|
|
95
100
|
}
|
|
96
101
|
}
|
|
97
102
|
|
package/src/lib/winsw.ts
CHANGED
|
@@ -72,7 +72,7 @@ export interface WinswEntry {
|
|
|
72
72
|
bun: string;
|
|
73
73
|
/** Provenance of `bun`, resolved together with it so the two can never disagree. */
|
|
74
74
|
bunRuntimeSource: BunRuntimeSource;
|
|
75
|
-
cli: string;
|
|
75
|
+
cli: string | null;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/**
|
|
@@ -118,7 +118,7 @@ export function buildWinswXml(entry: WinswEntry, env: NodeJS.ProcessEnv = proces
|
|
|
118
118
|
<name>OpenCodex Proxy (native)</name>
|
|
119
119
|
<description>OpenCodex proxy running as a native Windows service (windowless, starts at boot).</description>
|
|
120
120
|
<executable>${xmlEscape(entry.bun)}</executable>
|
|
121
|
-
<arguments>${xmlEscape(`"${entry.cli}" start --port ${safeListenPort}`)}</arguments>
|
|
121
|
+
<arguments>${xmlEscape(`${entry.cli ? `"${entry.cli}" ` : ""}start --port ${safeListenPort}`)}</arguments>
|
|
122
122
|
${envLines.join("\n")}
|
|
123
123
|
<logpath>${xmlEscape(winswLogDir())}</logpath>
|
|
124
124
|
<log mode="roll-by-size">
|
|
@@ -252,9 +252,10 @@ export abstract class OAuthCallbackFlow {
|
|
|
252
252
|
if (!parsed.code) return null;
|
|
253
253
|
// Kind-aware state enforcement: url/query-shaped input is an authorization
|
|
254
254
|
// RESPONSE and must carry a matching state — missing state is rejected, not
|
|
255
|
-
// downgraded to raw.
|
|
256
|
-
//
|
|
257
|
-
|
|
255
|
+
// downgraded to raw. A raw paste with an explicit code#state suffix is
|
|
256
|
+
// state-bearing too. Only a syntactically raw code WITHOUT a state suffix
|
|
257
|
+
// (same PKCE session) is exempt, so the CLI/GUI paste fallback still works.
|
|
258
|
+
if ((parsed.kind !== "raw" || parsed.state !== undefined) && expectedState && parsed.state !== expectedState) return null;
|
|
258
259
|
return { code: parsed.code, state: parsed.state ?? expectedState };
|
|
259
260
|
})
|
|
260
261
|
.catch((): CallbackResult | null => null),
|
|
@@ -234,6 +234,7 @@ function stableGenericRoster(providerName: string): string[] {
|
|
|
234
234
|
* quota-less provider off its active account on the very first request.
|
|
235
235
|
*/
|
|
236
236
|
function isOverAutoSwitchThreshold(providerName: string, accountId: string, threshold: number, requestedModelId?: string | null): boolean {
|
|
237
|
+
if (threshold <= 0) return false;
|
|
237
238
|
const headroom = accountHeadroomPercent(providerName, accountId, requestedModelId);
|
|
238
239
|
if (headroom === null) return false;
|
|
239
240
|
return 100 - headroom >= threshold;
|
package/src/oauth/health.ts
CHANGED
|
@@ -202,6 +202,7 @@ export function projectStoredOAuthAccountHealth(
|
|
|
202
202
|
export function projectCodexAccountHealth(input: {
|
|
203
203
|
accountId: string;
|
|
204
204
|
needsReauth: boolean;
|
|
205
|
+
reauthReason?: "unauthorized" | "forbidden" | "refresh_failed";
|
|
205
206
|
now?: number;
|
|
206
207
|
}): OAuthAccountHealth {
|
|
207
208
|
// One read serves every verdict below. Each lookup re-reads and re-hardens the whole store
|
|
@@ -230,6 +231,16 @@ export function projectCodexAccountHealth(input: {
|
|
|
230
231
|
|
|
231
232
|
const needsReauth = input.needsReauth || validationAuthFailed || terminalGrantFailure;
|
|
232
233
|
|
|
234
|
+
// A stored verification failure carries its own cause. When the caller did not name a
|
|
235
|
+
// reason, surface the persisted one instead of flattening every verdict into
|
|
236
|
+
// `refresh_failed`.
|
|
237
|
+
const storedVerificationFailed = validationAuthFailed || terminalGrantFailure;
|
|
238
|
+
const storedFailureReason: "unauthorized" | "forbidden" | undefined = !storedVerificationFailed
|
|
239
|
+
? undefined
|
|
240
|
+
: record?.lastCodexValidationError === "http_status:403" ? "forbidden"
|
|
241
|
+
: record?.lastCodexValidationError === "http_status:401" ? "unauthorized"
|
|
242
|
+
: undefined;
|
|
243
|
+
|
|
233
244
|
// Deferred validation is only worth reporting while the credential itself is still viable. A
|
|
234
245
|
// revoked grant needs a re-login, not a "Refresh quotas" click, so reauth is resolved first.
|
|
235
246
|
if (!needsReauth && record?.codexValidationPending) {
|
|
@@ -239,7 +250,7 @@ export function projectCodexAccountHealth(input: {
|
|
|
239
250
|
const snap = getCodexAccountHealthSnapshot(input.accountId, now);
|
|
240
251
|
return projectOAuthAccountHealth({
|
|
241
252
|
needsReauth,
|
|
242
|
-
reauthReason: needsReauth ? "refresh_failed" : undefined,
|
|
253
|
+
reauthReason: needsReauth ? (input.reauthReason ?? storedFailureReason ?? "refresh_failed") : undefined,
|
|
243
254
|
cooldownUntilMs: snap?.cooldownUntil,
|
|
244
255
|
cooldownReason: cooldownReasonFromSource(snap?.cooldownSource),
|
|
245
256
|
now,
|
package/src/oauth/index.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { KiroOAuthMetadata, OAuthController, OAuthCredentials } from "./types";
|
|
2
2
|
import { initializeProviderModelSelection } from "../providers/initial-model-selection";
|
|
3
|
-
import { parseCallbackInput } from "./callback-server";
|
|
4
3
|
import type { OcxConfig, OcxProviderConfig, RefreshPolicy } from "../types";
|
|
5
4
|
import { ConfigMutationLockError, loadConfig, mutatePersistedConfig, saveConfig } from "../config";
|
|
6
5
|
import { resolveProviderApiKey } from "../providers/key-store";
|
|
@@ -51,8 +50,9 @@ import { providerModelsUrl, resolveProviderModelDiscoveryUrl } from "../provider
|
|
|
51
50
|
import { resolveProviderTransport } from "../providers/xai-transport";
|
|
52
51
|
import { detectClaudeCodeToken, detectGrokCliToken, hasComparableGrokIdentity, isSameGrokIdentity, shouldAdoptGrokGeneration } from "./local-token-detect";
|
|
53
52
|
import { logOAuthEvent } from "./log";
|
|
54
|
-
import { captureConfigGeneration, sweepExpiredOnWrite
|
|
55
|
-
import {
|
|
53
|
+
import { captureConfigGeneration, sweepExpiredOnWrite } from "../lib/state-store-sweeper";
|
|
54
|
+
import { clearManualCodeSlot, ensureManualCodeSlot, kiroLoginSettling, loginAbort, loginState, waitForManualLoginCode } from "./login-flow-state";
|
|
55
|
+
export { reconcileOAuthFlowState, submitManualLoginCode } from "./login-flow-state";
|
|
56
56
|
import { randomUUID } from "node:crypto";
|
|
57
57
|
export {
|
|
58
58
|
CODEX_HEALTH_AUTH_FAILED_NOTE,
|
|
@@ -1273,6 +1273,29 @@ const OAUTH_RECONCILE_FIELDS: (keyof OcxProviderConfig)[] = [
|
|
|
1273
1273
|
// existing rows through enrichProviderFromRegistry, which is fill-only and
|
|
1274
1274
|
// preserves explicit saved values.
|
|
1275
1275
|
|
|
1276
|
+
/**
|
|
1277
|
+
* Output-budget fields an OAuth preset may refresh but must never erase.
|
|
1278
|
+
*
|
|
1279
|
+
* These stay on the reconcile list so a preset that does declare a budget still
|
|
1280
|
+
* refreshes the saved row. What changes is the other branch: when the preset
|
|
1281
|
+
* declares nothing, the operator's value survives instead of being deleted.
|
|
1282
|
+
*
|
|
1283
|
+
* Without that, the fields behaved as if they could not be configured at all.
|
|
1284
|
+
* No OAuth preset seeds either one, so the delete branch was the only branch
|
|
1285
|
+
* these two ever took, and a hand-edited `defaultMaxOutputTokens` was gone
|
|
1286
|
+
* before the first turn of the next startup — leaving the adapter's own
|
|
1287
|
+
* fallback as the only reachable output cap (#5190).
|
|
1288
|
+
*
|
|
1289
|
+
* Scoped to the output budget on purpose. The input side (`contextWindow`,
|
|
1290
|
+
* `modelContextWindows`) describes what the account's models are, which the
|
|
1291
|
+
* preset and live discovery do own; an output budget is a spend decision the
|
|
1292
|
+
* operator makes.
|
|
1293
|
+
*/
|
|
1294
|
+
const OAUTH_PRESERVE_WHEN_PRESET_UNSET: ReadonlySet<keyof OcxProviderConfig> = new Set([
|
|
1295
|
+
"defaultMaxOutputTokens",
|
|
1296
|
+
"modelMaxOutputTokens",
|
|
1297
|
+
]);
|
|
1298
|
+
|
|
1276
1299
|
const GOOGLE_ANTIGRAVITY_PROVIDER = "google-antigravity";
|
|
1277
1300
|
const GOOGLE_ANTIGRAVITY_LIVE_DISCOVERY_VERSION = 2 as const;
|
|
1278
1301
|
|
|
@@ -1312,7 +1335,7 @@ function applyOAuthPresetCatalog(
|
|
|
1312
1335
|
if (JSON.stringify(provider[field]) === JSON.stringify(preset[field])) continue;
|
|
1313
1336
|
if (preset[field] !== undefined) {
|
|
1314
1337
|
provider[field] = cloneProviderField(preset[field]) as never;
|
|
1315
|
-
} else {
|
|
1338
|
+
} else if (!OAUTH_PRESERVE_WHEN_PRESET_UNSET.has(field)) {
|
|
1316
1339
|
delete provider[field];
|
|
1317
1340
|
}
|
|
1318
1341
|
}
|
|
@@ -1710,110 +1733,6 @@ export async function runLogin(
|
|
|
1710
1733
|
* localhost), the GUI can POST the final redirect URL or authorization code via
|
|
1711
1734
|
* submitManualLoginCode(), which feeds OAuthController.onManualCodeInput.
|
|
1712
1735
|
*/
|
|
1713
|
-
const loginState = new Map<string, { error?: string; done: boolean }>();
|
|
1714
|
-
const loginAbort = new Map<string, AbortController>();
|
|
1715
|
-
const kiroLoginSettling = new Set<string>();
|
|
1716
|
-
|
|
1717
|
-
/** Pending paste for a login in progress: either a waiter or a stashed early submission. */
|
|
1718
|
-
interface ManualCodeSlot {
|
|
1719
|
-
pendingInput?: string;
|
|
1720
|
-
resolve?: (value: string) => void;
|
|
1721
|
-
/** Registered by the callback flow so submits can validate state synchronously. */
|
|
1722
|
-
expectedState?: string;
|
|
1723
|
-
}
|
|
1724
|
-
const loginManual = new Map<string, ManualCodeSlot>();
|
|
1725
|
-
const OAUTH_PENDING_CODE_MAX_BYTES = 4 * 1024;
|
|
1726
|
-
let lastOAuthFlowReconciledGeneration = 0;
|
|
1727
|
-
|
|
1728
|
-
export function reconcileOAuthFlowState(context: GenerationContext): number {
|
|
1729
|
-
if (context.generation <= lastOAuthFlowReconciledGeneration) return 0;
|
|
1730
|
-
let removed = 0;
|
|
1731
|
-
for (const [provider, state] of loginState) {
|
|
1732
|
-
if (context.providerNames.has(provider) || !state.done || loginAbort.has(provider)) continue;
|
|
1733
|
-
if (loginState.delete(provider)) removed += 1;
|
|
1734
|
-
if (loginManual.delete(provider)) removed += 1;
|
|
1735
|
-
if (loginAbort.delete(provider)) removed += 1;
|
|
1736
|
-
}
|
|
1737
|
-
lastOAuthFlowReconciledGeneration = context.generation;
|
|
1738
|
-
return removed;
|
|
1739
|
-
}
|
|
1740
|
-
|
|
1741
|
-
function clearManualCodeSlot(provider: string): void {
|
|
1742
|
-
loginManual.delete(provider);
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
|
-
function ensureManualCodeSlot(provider: string): ManualCodeSlot {
|
|
1746
|
-
let slot = loginManual.get(provider);
|
|
1747
|
-
if (!slot) {
|
|
1748
|
-
slot = {};
|
|
1749
|
-
loginManual.set(provider, slot);
|
|
1750
|
-
}
|
|
1751
|
-
return slot;
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
/** Wait for a GUI/CLI paste of the OAuth redirect URL or code (or return a stashed early submit). */
|
|
1755
|
-
function waitForManualLoginCode(provider: string, signal: AbortSignal, expectedState?: string): Promise<string> {
|
|
1756
|
-
if (signal.aborted) {
|
|
1757
|
-
return Promise.reject(new Error(`OAuth callback cancelled: ${signal.reason}`));
|
|
1758
|
-
}
|
|
1759
|
-
const slot = ensureManualCodeSlot(provider);
|
|
1760
|
-
if (expectedState !== undefined) slot.expectedState = expectedState;
|
|
1761
|
-
if (slot.pendingInput !== undefined) {
|
|
1762
|
-
const value = slot.pendingInput;
|
|
1763
|
-
slot.pendingInput = undefined;
|
|
1764
|
-
return Promise.resolve(value);
|
|
1765
|
-
}
|
|
1766
|
-
return new Promise<string>((resolve, reject) => {
|
|
1767
|
-
const onAbort = () => {
|
|
1768
|
-
if (slot.resolve === resolve) slot.resolve = undefined;
|
|
1769
|
-
reject(new Error(`OAuth callback cancelled: ${signal.reason}`));
|
|
1770
|
-
};
|
|
1771
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
1772
|
-
slot.resolve = (value: string) => {
|
|
1773
|
-
signal.removeEventListener("abort", onAbort);
|
|
1774
|
-
if (slot.resolve === resolve) slot.resolve = undefined;
|
|
1775
|
-
resolve(value);
|
|
1776
|
-
};
|
|
1777
|
-
});
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
/**
|
|
1781
|
-
* Feed a pasted redirect URL or authorization code into an in-progress GUI login.
|
|
1782
|
-
* Returns ok:false when no login is waiting (or input is empty). Invalid pastes are accepted
|
|
1783
|
-
* here and re-prompted by the OAuth callback loop if they cannot be parsed / fail state checks.
|
|
1784
|
-
*/
|
|
1785
|
-
export function submitManualLoginCode(provider: string, input: string): { ok: true } | { ok: false; error: string } {
|
|
1786
|
-
const trimmed = input.trim();
|
|
1787
|
-
if (!trimmed) return { ok: false, error: "empty code" };
|
|
1788
|
-
if (retainedUtf8Bytes(trimmed) > OAUTH_PENDING_CODE_MAX_BYTES) return { ok: false, error: "code too large" };
|
|
1789
|
-
const st = loginState.get(provider);
|
|
1790
|
-
if (!st || st.done) return { ok: false, error: "no login in progress" };
|
|
1791
|
-
const slot = ensureManualCodeSlot(provider);
|
|
1792
|
-
// Synchronous validation (validated request/ack): reject un-parseable input and
|
|
1793
|
-
// authorization responses (url/query kind) whose state is missing or mismatched
|
|
1794
|
-
// once the flow has registered its expected state. Raw codes stay in-session-PKCE
|
|
1795
|
-
// protected. Early posts (flow not yet waiting, no expectedState) are stashed and
|
|
1796
|
-
// re-validated by the callback loop.
|
|
1797
|
-
const parsed = parseCallbackInput(trimmed);
|
|
1798
|
-
// Command Code's manual fallback accepts a pasted JSON callback payload
|
|
1799
|
-
// (`{ apiKey, state, ... }`) which has no `code` param. Let it through the
|
|
1800
|
-
// shared gate so the provider-specific parser can validate it.
|
|
1801
|
-
const isCommandCodeJson = provider === "command-code" && trimmed.startsWith("{") && !parsed.code;
|
|
1802
|
-
if (!parsed.code && !isCommandCodeJson) return { ok: false, error: "no authorization code found in input" };
|
|
1803
|
-
if (parsed.kind !== "raw" && slot.expectedState !== undefined) {
|
|
1804
|
-
if (parsed.state === undefined) return { ok: false, error: "redirect URL is missing the state parameter" };
|
|
1805
|
-
if (parsed.state !== slot.expectedState) return { ok: false, error: "state mismatch — paste the redirect URL from THIS login attempt" };
|
|
1806
|
-
}
|
|
1807
|
-
if (slot.resolve) {
|
|
1808
|
-
const resolve = slot.resolve;
|
|
1809
|
-
slot.resolve = undefined;
|
|
1810
|
-
resolve(trimmed);
|
|
1811
|
-
} else {
|
|
1812
|
-
// Race: GUI may POST before the flow reaches onManualCodeInput — stash for the waiter.
|
|
1813
|
-
slot.pendingInput = trimmed;
|
|
1814
|
-
}
|
|
1815
|
-
return { ok: true };
|
|
1816
|
-
}
|
|
1817
1736
|
|
|
1818
1737
|
export interface OAuthAccountSummary {
|
|
1819
1738
|
id: string;
|