@iislee/opencodex 2.31.0 → 2.35.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 +3 -3
- package/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-Be1sDpWw.css +1 -0
- package/gui/dist/assets/index-D4OKgUV4.js +114 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/adapters/anthropic.ts +104 -15
- package/src/adapters/base.ts +29 -1
- package/src/adapters/command-code.ts +42 -6
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +69 -18
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +290 -0
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +14 -0
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +2 -2
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -2
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +68 -14
- package/src/adapters/cursor/request-builder.ts +40 -10
- package/src/adapters/cursor/tool-definitions.ts +14 -4
- package/src/adapters/cursor/tool-result-normalize.ts +28 -5
- package/src/adapters/cursor/types.ts +20 -0
- package/src/adapters/cursor.ts +209 -38
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +173 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google.ts +136 -24
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +27 -0
- package/src/adapters/kiro-tools.ts +55 -16
- package/src/adapters/kiro.ts +182 -23
- package/src/adapters/openai-chat.ts +300 -242
- package/src/adapters/openai-responses.ts +353 -27
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +4 -3
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/adapters/xai-web-search.ts +17 -16
- package/src/bridge.ts +90 -37
- package/src/chat/inbound.ts +14 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/context-windows.ts +16 -9
- package/src/claude/outbound.ts +24 -11
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +116 -19
- package/src/cli/account-extended.ts +262 -29
- package/src/cli/account-main.ts +12 -12
- package/src/cli/account.ts +69 -15
- package/src/cli/agent.ts +8 -1
- package/src/cli/alias.ts +66 -0
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +496 -0
- package/src/cli/claude-desktop.ts +31 -11
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +256 -28
- package/src/cli/doctor.ts +126 -14
- package/src/cli/help.ts +17 -6
- package/src/cli/index.ts +39 -9
- package/src/cli/init.ts +1 -0
- package/src/cli/inspect.ts +230 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +26 -10
- package/src/cli/observe.ts +31 -8
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/provider.ts +2 -1
- package/src/cli/registry.ts +49 -10
- package/src/cli/runtime-api.ts +51 -7
- package/src/cli/status.ts +41 -1
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +184 -0
- package/src/cli/v2.ts +34 -10
- package/src/cli/version-skew.ts +46 -0
- package/src/clients/config-export.ts +85 -3
- package/src/clients/effective-status.ts +30 -2
- package/src/clients/probes/cline.ts +81 -0
- package/src/codex/account-label.ts +21 -0
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +83 -7
- package/src/codex/auth-api.ts +50 -10
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/autostart-health.ts +16 -0
- package/src/codex/catalog/aggregation.ts +18 -0
- package/src/codex/catalog/effort.ts +18 -3
- package/src/codex/catalog/metadata.ts +27 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +72 -27
- package/src/codex/catalog/provider-fetch.ts +187 -31
- package/src/codex/catalog/sync.ts +131 -5
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +27 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +355 -0
- package/src/codex/history-job.ts +32 -3
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +5 -5
- package/src/codex/history-provider.ts +825 -247
- package/src/codex/history-worker.ts +8 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +63 -22
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +91 -10
- package/src/codex/routing.ts +9 -0
- package/src/codex/shim.ts +56 -3
- package/src/codex/subagent-model-fallback.ts +104 -25
- package/src/codex/transition-state.ts +116 -8
- package/src/combos/failover.ts +27 -0
- package/src/combos/types.ts +10 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +121 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +309 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +206 -824
- package/src/generated/compatibility-version.json +373 -213
- package/src/images/loop.ts +5 -4
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +160 -0
- package/src/integrations/ownership.ts +46 -2
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/state.ts +79 -7
- package/src/integrations/writer.ts +26 -2
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +36 -3
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/lib/process-control.ts +2 -1
- package/src/lib/service-secrets.ts +15 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/callback-server.ts +22 -2
- package/src/oauth/cline.ts +203 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/health.ts +1 -1
- package/src/oauth/index.ts +65 -6
- package/src/oauth/key-providers.ts +10 -1
- package/src/oauth/kimi.ts +9 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/local-token-detect.ts +2 -0
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +14 -5
- package/src/oauth/types.ts +15 -0
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +28 -0
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/label.ts +34 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +65 -7
- package/src/providers/registry.ts +184 -69
- package/src/providers/slug-codec.ts +52 -0
- package/src/providers/xai-transport.ts +21 -0
- package/src/reasoning-effort.ts +19 -2
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +157 -39
- package/src/responses/namespace-tool-compat.ts +84 -4
- package/src/responses/parser.ts +7 -2
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/responses/schema.ts +5 -1
- package/src/responses/state.ts +94 -4
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/responses/turn-termination.ts +107 -0
- package/src/router.ts +40 -2
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +27 -7
- package/src/server/chat-completions.ts +24 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/index.ts +179 -32
- package/src/server/lifecycle.ts +52 -1
- package/src/server/local-management-read-client.ts +1 -1
- package/src/server/local-provider-reload-client.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +45 -23
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +59 -12
- package/src/server/management/context.ts +10 -1
- package/src/server/management/logs-usage-routes.ts +30 -24
- package/src/server/management/model-routes.ts +274 -4
- package/src/server/management/model-rows.ts +4 -0
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/oauth-account-routes.ts +10 -4
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/provider-routes.ts +81 -11
- package/src/server/management/route-registry.ts +332 -0
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/management-api.ts +18 -16
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +29 -5
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +51 -0
- package/src/server/request-log.ts +70 -16
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/collaboration.ts +37 -3
- package/src/server/responses/combo-stream-preflight.ts +178 -0
- package/src/server/responses/compact.ts +6 -21
- package/src/server/responses/core.ts +810 -208
- package/src/server/responses/empty-completion-guard.ts +63 -6
- package/src/server/responses/fetch-helpers.ts +17 -102
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses/ws-upstream.ts +75 -2
- package/src/server/responses-custom-tool-repair.ts +87 -6
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/responses-undeclared-tool-guard.ts +241 -18
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/service.ts +42 -4
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +88 -23
- package/src/types/config.ts +66 -0
- package/src/types/provider.ts +69 -0
- package/src/types/request.ts +17 -4
- package/src/types/tools.ts +116 -11
- package/src/types.ts +3 -1
- package/src/update/index.ts +5 -4
- package/src/update/job.ts +3 -1
- package/src/update/transactional-install.mjs +8 -1
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +63 -12
- package/src/usage/summary.ts +211 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/executor.ts +10 -3
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-BF38heuV.js +0 -104
- package/gui/dist/assets/index-DMiI18Kv.css +0 -1
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic OAuth multi-account 429 failover (#2568).
|
|
3
|
+
*
|
|
4
|
+
* The API-key twin (`providers/key-failover.ts`) rotates by default for any key provider with a
|
|
5
|
+
* 2+ pool, but it returns false for `authMode === "oauth"`, and the only OAuth rotator that
|
|
6
|
+
* exists is Anthropic's — behind its own opt-in. So xAI, Cursor, Kimi, GitHub Copilot,
|
|
7
|
+
* Antigravity and Nous have no recovery path on a 429 even with several accounts logged in.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately narrower than the Anthropic pool: no session affinity, no quota-ranked selection,
|
|
10
|
+
* no probe leases. Those carry provider-specific meaning; this module only answers "the account
|
|
11
|
+
* that just 429'd is cooled, is there another one we may use".
|
|
12
|
+
*
|
|
13
|
+
* NOT a home for Codex (`codex/routing.ts` owns quota scopes and probe leases) or Anthropic
|
|
14
|
+
* (`oauth/anthropic-routing.ts` owns affinity and a fail-closed local-cli credential rule).
|
|
15
|
+
* Both are excluded by `isGenericFailoverProvider`.
|
|
16
|
+
*/
|
|
17
|
+
import { getAccountSet } from "./store";
|
|
18
|
+
import { getValidAccessSnapshotForAccount, type OAuthAccessSnapshot } from "./index";
|
|
19
|
+
import { parseRetryAfterMs } from "../combos/failover";
|
|
20
|
+
import { sweepExpiredOnWrite } from "../lib/state-store-sweeper";
|
|
21
|
+
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
22
|
+
|
|
23
|
+
/** Cap same-request rotations so a short Retry-After cannot spin. Mirrors the Anthropic bound. */
|
|
24
|
+
export const GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST = 3;
|
|
25
|
+
|
|
26
|
+
const DEFAULT_COOLDOWN_MS = 60_000;
|
|
27
|
+
const MAX_COOLDOWN_MS = 15 * 60_000;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* How long a presence answer may be reused before the store is consulted again.
|
|
31
|
+
*
|
|
32
|
+
* `loadAuthStore` has no cache: every call chmods the config dir and the secret, reads the whole
|
|
33
|
+
* file, parses it and normalizes the store (store.ts:136-151). Since presence now decides
|
|
34
|
+
* activation, this predicate runs on paths that have not seen a 429 at all — the streaming and
|
|
35
|
+
* non-streaming runTurn entry points evaluate it once per request — so an uncached check would put
|
|
36
|
+
* a synchronous file read in front of every request for every OAuth provider.
|
|
37
|
+
*
|
|
38
|
+
* Two seconds is short enough that a login in another window is picked up before the operator can
|
|
39
|
+
* switch back and send a prompt, and long enough that a burst of requests shares one read. The
|
|
40
|
+
* cache holds a COUNT, never a credential.
|
|
41
|
+
*/
|
|
42
|
+
const PRESENCE_CACHE_TTL_MS = 2_000;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Providers whose rotation is owned elsewhere and must not be handled here.
|
|
46
|
+
*
|
|
47
|
+
* `openai` is the Codex pool: quota scopes, probe leases and affinity semantics that this
|
|
48
|
+
* module deliberately does not reimplement. `anthropic` has its own pool with a fail-closed
|
|
49
|
+
* rule about background local-cli credential slots.
|
|
50
|
+
*/
|
|
51
|
+
const EXCLUDED_PROVIDERS = new Set(["openai", "anthropic"]);
|
|
52
|
+
|
|
53
|
+
interface AccountHealth {
|
|
54
|
+
cooldownUntil: number;
|
|
55
|
+
cooldownSource: "retry-after" | "default";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface PresenceEntry {
|
|
59
|
+
eligible: number;
|
|
60
|
+
readAt: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Process-local, like the Anthropic pool's: a restart is allowed to forget a cooldown. */
|
|
64
|
+
const health = new Map<string, AccountHealth>();
|
|
65
|
+
|
|
66
|
+
/** Provider -> recent eligible-account count. TTL-bounded; never holds credential material. */
|
|
67
|
+
const presence = new Map<string, PresenceEntry>();
|
|
68
|
+
|
|
69
|
+
const healthKey = (provider: string, accountId: string) => `${provider}\u0000${accountId}`;
|
|
70
|
+
|
|
71
|
+
function isCooled(provider: string, accountId: string, now: number): boolean {
|
|
72
|
+
const entry = health.get(healthKey(provider, accountId));
|
|
73
|
+
if (!entry) return false;
|
|
74
|
+
if (entry.cooldownUntil <= now) {
|
|
75
|
+
health.delete(healthKey(provider, accountId));
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** True when this provider participates in generic rotation at all. */
|
|
82
|
+
export function isGenericFailoverProvider(providerName: string, provider: OcxProviderConfig): boolean {
|
|
83
|
+
return provider.authMode === "oauth" && !EXCLUDED_PROVIDERS.has(providerName);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Stored accounts that could serve traffic if asked, ignoring cooldowns.
|
|
88
|
+
*
|
|
89
|
+
* Cooldowns are excluded on purpose: they are transient and per-request, while this answers the
|
|
90
|
+
* durable question "did the operator log in more than one account". Treating a cooled account as
|
|
91
|
+
* absent would switch the feature off for the rest of the cooldown, which is exactly when it is
|
|
92
|
+
* needed.
|
|
93
|
+
*/
|
|
94
|
+
function eligibleAccountCount(providerName: string, now: number): number {
|
|
95
|
+
const cached = presence.get(providerName);
|
|
96
|
+
if (cached && now >= cached.readAt && now - cached.readAt < PRESENCE_CACHE_TTL_MS) return cached.eligible;
|
|
97
|
+
const set = getAccountSet(providerName);
|
|
98
|
+
const eligible = set ? set.accounts.filter(account => account.needsReauth !== true).length : 0;
|
|
99
|
+
presence.set(providerName, { eligible, readAt: now });
|
|
100
|
+
return eligible;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Presence IS consent (#2568d).
|
|
105
|
+
*
|
|
106
|
+
* `hasKeyPoolFailover` already reads a 2+ key pool as the operator asking for rotation, and a
|
|
107
|
+
* second OAuth login is the same statement. One account stays a strict no-op either way, so this
|
|
108
|
+
* only changes behaviour for someone who deliberately logged in twice.
|
|
109
|
+
*/
|
|
110
|
+
export function hasFailoverAccountQuorum(providerName: string, now = Date.now()): boolean {
|
|
111
|
+
return eligibleAccountCount(providerName, now) >= 2;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Whether generic rotation is active for this provider.
|
|
116
|
+
*
|
|
117
|
+
* Precedence, most specific first:
|
|
118
|
+
*
|
|
119
|
+
* 1. `providers.<name>.oauthAccountFailover.enabled` — an operator may accept rotation on one
|
|
120
|
+
* provider and refuse it on another, because provider terms differ.
|
|
121
|
+
* 2. `oauthAccountFailover.enabled` — the global switch. Anyone who already wrote `false` keeps
|
|
122
|
+
* strict single-account behaviour across this change.
|
|
123
|
+
* 3. Presence: 2 or more eligible stored accounts (#2568d, owner decision).
|
|
124
|
+
*
|
|
125
|
+
* Only an explicit boolean overrides presence. A malformed value falls through instead of
|
|
126
|
+
* throwing, because a typo in a knob must not take a provider out of service.
|
|
127
|
+
*/
|
|
128
|
+
export function isGenericOAuthFailoverEnabled(
|
|
129
|
+
config: OcxConfig,
|
|
130
|
+
providerName: string,
|
|
131
|
+
now = Date.now(),
|
|
132
|
+
): boolean {
|
|
133
|
+
const provider = config.providers?.[providerName];
|
|
134
|
+
if (!provider || !isGenericFailoverProvider(providerName, provider)) return false;
|
|
135
|
+
const perProvider = provider.oauthAccountFailover?.enabled;
|
|
136
|
+
if (typeof perProvider === "boolean") return perProvider;
|
|
137
|
+
const global = config.oauthAccountFailover?.enabled;
|
|
138
|
+
if (typeof global === "boolean") return global;
|
|
139
|
+
return hasFailoverAccountQuorum(providerName, now);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Accounts that may serve traffic right now: not cooled, not flagged for reauth. */
|
|
143
|
+
export function eligibleFailoverAccounts(providerName: string, now = Date.now()): string[] {
|
|
144
|
+
const set = getAccountSet(providerName);
|
|
145
|
+
if (!set) return [];
|
|
146
|
+
return set.accounts
|
|
147
|
+
.filter(account => account.needsReauth !== true && !isCooled(providerName, account.id, now))
|
|
148
|
+
.map(account => account.id);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Cool the account that actually 429'd and name the next eligible one, or null.
|
|
153
|
+
*
|
|
154
|
+
* Returns the id only; the caller mints the credential so a failed refresh does not leave the
|
|
155
|
+
* cooldown applied to an account we then could not use.
|
|
156
|
+
*/
|
|
157
|
+
export function rotateGenericOAuthAccountOn429(
|
|
158
|
+
config: OcxConfig,
|
|
159
|
+
providerName: string,
|
|
160
|
+
failedAccountId: string,
|
|
161
|
+
retryAfterHeader: string | null | undefined,
|
|
162
|
+
now = Date.now(),
|
|
163
|
+
): string | null {
|
|
164
|
+
if (!isGenericOAuthFailoverEnabled(config, providerName)) return null;
|
|
165
|
+
const set = getAccountSet(providerName);
|
|
166
|
+
// A single stored account has nowhere to go; rotating to itself would just replay the 429.
|
|
167
|
+
if (!set || set.accounts.length < 2) return null;
|
|
168
|
+
|
|
169
|
+
const parsed = parseRetryAfterMs(retryAfterHeader, now);
|
|
170
|
+
const cooldownMs = Math.min(parsed ?? DEFAULT_COOLDOWN_MS, MAX_COOLDOWN_MS);
|
|
171
|
+
health.set(healthKey(providerName, failedAccountId), {
|
|
172
|
+
cooldownUntil: now + cooldownMs,
|
|
173
|
+
cooldownSource: parsed ? "retry-after" : "default",
|
|
174
|
+
});
|
|
175
|
+
sweepExpiredOnWrite(now);
|
|
176
|
+
|
|
177
|
+
const eligible = eligibleFailoverAccounts(providerName, now).filter(id => id !== failedAccountId);
|
|
178
|
+
if (eligible.length === 0) return null;
|
|
179
|
+
// A rotation means the roster in use just changed; do not answer the next activation question
|
|
180
|
+
// from a count read before the failure.
|
|
181
|
+
presence.delete(providerName);
|
|
182
|
+
// Deterministic: start after the failed account so repeated 429s walk the roster instead of
|
|
183
|
+
// hammering whichever id happens to sort first.
|
|
184
|
+
const order = set.accounts.map(account => account.id);
|
|
185
|
+
const start = order.indexOf(failedAccountId);
|
|
186
|
+
for (let i = 1; i <= order.length; i++) {
|
|
187
|
+
const candidate = order[(start + i) % order.length]!;
|
|
188
|
+
if (candidate !== failedAccountId && eligible.includes(candidate)) return candidate;
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Full credential snapshot for a rotated account.
|
|
195
|
+
*
|
|
196
|
+
* Returns the snapshot rather than a bare bearer: Antigravity pairs an account-matched
|
|
197
|
+
* `projectId` with its token and Kiro carries routing metadata, so a token-only swap would mix
|
|
198
|
+
* one account's bearer with another's routing data.
|
|
199
|
+
*/
|
|
200
|
+
export async function failoverAccountSnapshot(
|
|
201
|
+
providerName: string,
|
|
202
|
+
accountId: string,
|
|
203
|
+
): Promise<OAuthAccessSnapshot> {
|
|
204
|
+
return getValidAccessSnapshotForAccount(providerName, accountId);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Earliest remaining cooldown, for a client-facing Retry-After when every account is cooled. */
|
|
208
|
+
export function genericFailoverRetryAfterSeconds(providerName: string, now = Date.now()): number | null {
|
|
209
|
+
const set = getAccountSet(providerName);
|
|
210
|
+
if (!set) return null;
|
|
211
|
+
let earliest: number | null = null;
|
|
212
|
+
for (const account of set.accounts) {
|
|
213
|
+
const entry = health.get(healthKey(providerName, account.id));
|
|
214
|
+
if (!entry || entry.cooldownUntil <= now) continue;
|
|
215
|
+
if (earliest === null || entry.cooldownUntil < earliest) earliest = entry.cooldownUntil;
|
|
216
|
+
}
|
|
217
|
+
return earliest === null ? null : Math.max(1, Math.ceil((earliest - now) / 1000));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Test seam and manual-recovery hook. */
|
|
221
|
+
export function clearGenericFailoverHealth(providerName?: string): void {
|
|
222
|
+
if (!providerName) {
|
|
223
|
+
health.clear();
|
|
224
|
+
presence.clear();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
presence.delete(providerName);
|
|
228
|
+
for (const key of [...health.keys()]) {
|
|
229
|
+
if (key.startsWith(`${providerName}\u0000`)) health.delete(key);
|
|
230
|
+
}
|
|
231
|
+
}
|
package/src/oauth/health.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { getAnthropicAccountHealthSnapshot } from "./anthropic-routing";
|
|
|
3
3
|
import { isAccountNeedsReauth } from "../codex/account-runtime-state";
|
|
4
4
|
import { getCodexAccountCredential, listCodexAccountIds } from "../codex/account-store";
|
|
5
5
|
import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
|
|
6
|
-
import { readRuntimePort } from "../config";
|
|
6
|
+
import { readRuntimePort } from "../config/process-state";
|
|
7
7
|
import { LOCAL_MANAGEMENT_READ_PATHS } from "../lib/local-management-capability";
|
|
8
8
|
import { maskAccountId } from "../lib/privacy";
|
|
9
9
|
import { findLiveProxy } from "../server/proxy-liveness";
|
package/src/oauth/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { loginAntigravity, refreshAntigravityToken } from "./google-antigravity"
|
|
|
14
14
|
import { loginCursor, refreshCursorToken } from "./cursor";
|
|
15
15
|
import { loginGithubCopilot, refreshGithubCopilotToken, validateCopilotApiBaseUrl } from "./github-copilot";
|
|
16
16
|
import { loginCommandCode, refreshCommandCodeToken } from "./command-code";
|
|
17
|
+
import { loginCline, refreshClineToken } from "./cline";
|
|
17
18
|
import { ANTIGRAVITY_REQUEST_UA } from "../adapters/google-antigravity-wire";
|
|
18
19
|
import { deriveOAuthDefaultModel, deriveOAuthProviderConfig } from "../providers/derive";
|
|
19
20
|
import { apiKeyPoolEntryId, sanitizeApiKeyValue } from "../providers/api-keys";
|
|
@@ -58,11 +59,20 @@ export interface OAuthAccessSnapshot {
|
|
|
58
59
|
/** Cloud Code Assist project selected during Antigravity login. */
|
|
59
60
|
projectId?: string;
|
|
60
61
|
/** Safe request-routing subset; refresh-only Kiro client secrets never leave the credential store. */
|
|
61
|
-
kiro?: Pick<KiroOAuthMetadata, "profileArn" | "apiRegion" | "ssoRegion">;
|
|
62
|
+
kiro?: Pick<KiroOAuthMetadata, "profileArn" | "apiRegion" | "ssoRegion" | "authType">;
|
|
63
|
+
/**
|
|
64
|
+
* Allowlisted GitHub Copilot API origin belonging to THIS account.
|
|
65
|
+
*
|
|
66
|
+
* Copilot pins its bearer to an account-scoped regional host, and the initial route already
|
|
67
|
+
* pairs the two (`core.ts` resolves transport with `getOAuthCredentialApiBaseUrl`). Account
|
|
68
|
+
* failover must carry the pairing across the rotation; without it, account B's token is sent to
|
|
69
|
+
* account A's origin (#2568d).
|
|
70
|
+
*/
|
|
71
|
+
apiBaseUrl?: string;
|
|
62
72
|
}
|
|
63
73
|
|
|
64
74
|
export interface ObservedOAuthAccessSnapshot extends OAuthAccessSnapshot {
|
|
65
|
-
/**
|
|
75
|
+
/** Retained for callers that predate `apiBaseUrl` moving onto the base snapshot. */
|
|
66
76
|
apiBaseUrl?: string;
|
|
67
77
|
}
|
|
68
78
|
|
|
@@ -237,6 +247,12 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderDef> = {
|
|
|
237
247
|
// Unofficial Copilot bridge — keep proactive traffic lazy-only (no background guardian spam).
|
|
238
248
|
defaultRefreshPolicy: "lazy-only",
|
|
239
249
|
},
|
|
250
|
+
cline: {
|
|
251
|
+
login: (ctrl, opts) => loginCline(ctrl, { importLocal: opts?.forceLogin ? "off" : "fallback", forceLogin: opts?.forceLogin }),
|
|
252
|
+
refresh: refreshClineToken,
|
|
253
|
+
providerConfig: oauthConfig("cline"),
|
|
254
|
+
defaultModel: oauthDefaultModel("cline"),
|
|
255
|
+
},
|
|
240
256
|
chatgpt: {
|
|
241
257
|
login: loginChatGPT,
|
|
242
258
|
refresh: (rt) => refreshChatGPTToken(rt),
|
|
@@ -350,24 +366,43 @@ export function publicOAuthAuthenticationErrorMessage(error: unknown): string {
|
|
|
350
366
|
}
|
|
351
367
|
|
|
352
368
|
function accessSnapshot(provider: string, accountId: string, cred: OAuthCredentials): OAuthAccessSnapshot {
|
|
369
|
+
// Derived, not read back: a stored `authType` is trusted when present, but a credential imported
|
|
370
|
+
// before the field existed still routes correctly because the client pair implies SSO OIDC.
|
|
371
|
+
const kiroAuthType = cred.kiro?.authType
|
|
372
|
+
?? (cred.kiro?.clientId && cred.kiro?.clientSecret ? "aws_sso_oidc" as const : undefined);
|
|
353
373
|
const storedKiroRouting = {
|
|
354
374
|
...(cred.kiro?.profileArn ? { profileArn: cred.kiro.profileArn } : {}),
|
|
355
375
|
...(cred.kiro?.apiRegion ? { apiRegion: cred.kiro.apiRegion } : {}),
|
|
356
376
|
...(cred.kiro?.ssoRegion ? { ssoRegion: cred.kiro.ssoRegion } : {}),
|
|
357
377
|
};
|
|
378
|
+
// `authType` is a property OF the account, not routing the environment can substitute for, so it
|
|
379
|
+
// is merged after the environment fallback decision rather than counting as stored routing.
|
|
380
|
+
// Folding it into `storedKiroRouting` would make a client-pair-only credential look non-empty
|
|
381
|
+
// and silently disable `environmentKiroRoutingMetadata()` for it.
|
|
382
|
+
const kiroAuthTypeRouting = kiroAuthType ? { authType: kiroAuthType } : {};
|
|
383
|
+
// Validated here, not at the call site: an unvalidated origin from a legacy or crafted
|
|
384
|
+
// credential must never travel with a bearer, and dropping it makes the transport fall back to
|
|
385
|
+
// the canonical host rather than to whatever the previous account was using.
|
|
386
|
+
const copilotApiBaseUrl = provider === "github-copilot"
|
|
387
|
+
? validateCopilotApiBaseUrl(cred.apiBaseUrl)
|
|
388
|
+
: undefined;
|
|
358
389
|
return {
|
|
359
390
|
provider,
|
|
360
391
|
accountId,
|
|
361
392
|
generation: credentialGeneration(cred),
|
|
362
393
|
accessToken: cred.access,
|
|
363
394
|
...(cred.projectId ? { projectId: cred.projectId } : {}),
|
|
395
|
+
...(copilotApiBaseUrl ? { apiBaseUrl: copilotApiBaseUrl } : {}),
|
|
364
396
|
// Stored account metadata remains authoritative. Metadata-less legacy/environment credentials
|
|
365
397
|
// may use explicit environment routing, but never borrow the currently signed-in local CLI account.
|
|
366
398
|
...(provider === "kiro"
|
|
367
399
|
? {
|
|
368
|
-
kiro:
|
|
369
|
-
|
|
370
|
-
|
|
400
|
+
kiro: {
|
|
401
|
+
...(Object.keys(storedKiroRouting).length > 0
|
|
402
|
+
? storedKiroRouting
|
|
403
|
+
: environmentKiroRoutingMetadata() ?? {}),
|
|
404
|
+
...kiroAuthTypeRouting,
|
|
405
|
+
},
|
|
371
406
|
}
|
|
372
407
|
: {}),
|
|
373
408
|
};
|
|
@@ -467,7 +502,7 @@ export async function getValidAccessTokenSnapshot(provider: string): Promise<OAu
|
|
|
467
502
|
}
|
|
468
503
|
|
|
469
504
|
/** Providers whose upstream-401 replay path may force a snapshot refresh. */
|
|
470
|
-
const FORCE_REFRESH_PROVIDERS = new Set(["xai", "github-copilot", "kiro"]);
|
|
505
|
+
const FORCE_REFRESH_PROVIDERS = new Set(["xai", "github-copilot", "kiro", "cline"]);
|
|
471
506
|
|
|
472
507
|
export async function forceRefreshOAuthAccessSnapshot(
|
|
473
508
|
rejected: OAuthAccessSnapshot,
|
|
@@ -490,6 +525,22 @@ export async function getValidAccessTokenForAccount(provider: string, accountId:
|
|
|
490
525
|
return (await resolveAccessSnapshotForAccount(provider, accountId)).accessToken;
|
|
491
526
|
}
|
|
492
527
|
|
|
528
|
+
/**
|
|
529
|
+
* Account-scoped resolver returning the FULL snapshot, not just the bearer.
|
|
530
|
+
*
|
|
531
|
+
* A rotator that swaps only the token silently mixes credential generations: Antigravity pairs
|
|
532
|
+
* an account-matched `projectId` with its token (see the pairing comment in
|
|
533
|
+
* server/responses/core.ts), Kiro carries routing metadata, and Copilot's observed snapshot
|
|
534
|
+
* carries an account-specific API origin. Reading those back from "whichever account is active"
|
|
535
|
+
* after a rotation is exactly the mixing this returns in one piece to prevent (#2568).
|
|
536
|
+
*/
|
|
537
|
+
export async function getValidAccessSnapshotForAccount(
|
|
538
|
+
provider: string,
|
|
539
|
+
accountId: string,
|
|
540
|
+
): Promise<OAuthAccessSnapshot> {
|
|
541
|
+
return resolveAccessSnapshotForAccount(provider, accountId);
|
|
542
|
+
}
|
|
543
|
+
|
|
493
544
|
/** Terminal refresh failures (revoked/rotated-away grants) — retrying cannot succeed. */
|
|
494
545
|
function isTerminalRefreshError(err: unknown): boolean {
|
|
495
546
|
const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
|
|
@@ -1079,6 +1130,14 @@ export function upsertOAuthProvider(config: OcxConfig, provider: string): void {
|
|
|
1079
1130
|
if (existing?.modelCosts !== undefined) {
|
|
1080
1131
|
next.modelCosts = existing.modelCosts;
|
|
1081
1132
|
}
|
|
1133
|
+
// The per-provider account-failover opt-out is operator intent about SPENDING, and the login
|
|
1134
|
+
// path is exactly where losing it does damage: adding a second account both rebuilds this row
|
|
1135
|
+
// from the preset and creates the 2-account quorum that turns presence-driven rotation on
|
|
1136
|
+
// (#2568d). Dropping the opt-out here would enable the thing the operator switched off, at the
|
|
1137
|
+
// moment they were doing something unrelated.
|
|
1138
|
+
if (existing?.oauthAccountFailover !== undefined) {
|
|
1139
|
+
next.oauthAccountFailover = existing.oauthAccountFailover;
|
|
1140
|
+
}
|
|
1082
1141
|
if (existing && getProviderRegistryEntry(provider)?.allowKeyAuthOverride === true) {
|
|
1083
1142
|
// Shared sanitizeApiKeyValue trim / no-CRLF checks from api-key pool writes.
|
|
1084
1143
|
let storedApiKey = sanitizeApiKeyValue(existing.apiKey);
|
|
@@ -19,7 +19,8 @@ export const KEY_LOGIN_PROVIDERS: Record<string, KeyLoginProvider> = deriveKeyLo
|
|
|
19
19
|
* caller didn't already supply. Lets the vision/reasoning classification actually reach the saved
|
|
20
20
|
* config (the GUI/API only send adapter/baseUrl/apiKey/defaultModel). No-op for unknown names.
|
|
21
21
|
*
|
|
22
|
-
* `modelSupportsReasoningSummaries`
|
|
22
|
+
* `modelSupportsReasoningSummaries` and the verbosity capability are deliberately excluded from
|
|
23
|
+
* what gets persisted. They are
|
|
23
24
|
* registry-only metadata resolved at runtime, and this function feeds a config that is about to
|
|
24
25
|
* be written to disk. Persisting today's registry defaults would freeze them as the user's own
|
|
25
26
|
* overrides: a later registry correction — say we learn a model's backend rejects summary
|
|
@@ -30,9 +31,17 @@ export const KEY_LOGIN_PROVIDERS: Record<string, KeyLoginProvider> = deriveKeyLo
|
|
|
30
31
|
export function enrichProviderFromCatalog(name: string, prov: OcxProviderConfig): void {
|
|
31
32
|
const hadOwnSummaries = Object.hasOwn(prov, "modelSupportsReasoningSummaries");
|
|
32
33
|
const submittedSummaries = prov.modelSupportsReasoningSummaries;
|
|
34
|
+
const hadOwnVerbosity = Object.hasOwn(prov, "modelSupportsVerbosity");
|
|
35
|
+
const submittedVerbosity = prov.modelSupportsVerbosity;
|
|
36
|
+
const hadOwnProviderVerbosity = Object.hasOwn(prov, "supportsVerbosity");
|
|
37
|
+
const submittedProviderVerbosity = prov.supportsVerbosity;
|
|
33
38
|
enrichProviderFromRegistry(name, prov);
|
|
34
39
|
if (hadOwnSummaries) prov.modelSupportsReasoningSummaries = submittedSummaries;
|
|
35
40
|
else delete prov.modelSupportsReasoningSummaries;
|
|
41
|
+
if (hadOwnVerbosity) prov.modelSupportsVerbosity = submittedVerbosity;
|
|
42
|
+
else delete prov.modelSupportsVerbosity;
|
|
43
|
+
if (hadOwnProviderVerbosity) prov.supportsVerbosity = submittedProviderVerbosity;
|
|
44
|
+
else delete prov.supportsVerbosity;
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
export function isKeyLoginProvider(name: string): boolean {
|
package/src/oauth/kimi.ts
CHANGED
|
@@ -209,7 +209,15 @@ async function pollForToken(deviceCode: string, intervalMs: number, expiresInMs:
|
|
|
209
209
|
|
|
210
210
|
export async function loginKimi(ctrl: OAuthController): Promise<OAuthCredentials> {
|
|
211
211
|
const device = await requestDeviceAuthorization();
|
|
212
|
-
|
|
212
|
+
// `deviceCode` carries the human-typed user code, matching the other device
|
|
213
|
+
// providers. It is also what tells the management login route this is a device
|
|
214
|
+
// flow, so a provider-supplied verification URI is no longer handed to a local
|
|
215
|
+
// browser spawn — the URL stays visible and copyable in every login surface.
|
|
216
|
+
ctrl.onAuth?.({
|
|
217
|
+
url: device.verificationUriComplete,
|
|
218
|
+
instructions: `Enter code: ${device.userCode}`,
|
|
219
|
+
deviceCode: device.userCode,
|
|
220
|
+
});
|
|
213
221
|
return pollForToken(device.deviceCode, device.intervalMs, device.expiresInMs, ctrl.signal);
|
|
214
222
|
}
|
|
215
223
|
|
package/src/oauth/kiro.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
} from "./kiro-credentials";
|
|
29
29
|
import { homedir } from "node:os";
|
|
30
30
|
import { getAccountSet, saveAccountCredential } from "./store";
|
|
31
|
+
import { KIRO_BUILDER_ID_SERVICE_PROFILE_ARN } from "../adapters/kiro-constants";
|
|
31
32
|
|
|
32
33
|
const DEFAULT_REGION = "us-east-1";
|
|
33
34
|
const REFRESH_URL = "https://prod.{region}.auth.desktop.kiro.dev/refreshToken";
|
|
@@ -473,6 +474,50 @@ export function resolveKiroProfileArn(account?: Pick<KiroOAuthMetadata, "profile
|
|
|
473
474
|
return readImportedKiroCredential()?.profileArn;
|
|
474
475
|
}
|
|
475
476
|
|
|
477
|
+
/**
|
|
478
|
+
* Resolve the profileArn actually SENT upstream, which is not always the account's own.
|
|
479
|
+
*
|
|
480
|
+
* An AWS Builder ID account authenticates through SSO OIDC and never receives an account-scoped
|
|
481
|
+
* profile ARN, so gated models reject its requests with a `profileArn`-demanding
|
|
482
|
+
* `ValidationException`. The Kiro CLI handles this by carrying a fixed service profile on Builder
|
|
483
|
+
* ID requests, and this mirrors that.
|
|
484
|
+
*
|
|
485
|
+
* Deliberately separate from `resolveKiroProfileArn`: that resolver answers "what is this
|
|
486
|
+
* account's profile", and callers that ask it — region inference, account matching, continuation
|
|
487
|
+
* scoping — must keep receiving `undefined` here. Only request construction uses this function.
|
|
488
|
+
*
|
|
489
|
+
* The fallback is gated on `authType === "aws_sso_oidc"` rather than on a missing ARN, so a
|
|
490
|
+
* `kiro_desktop` account whose profile import failed keeps producing its actionable error instead
|
|
491
|
+
* of silently borrowing a service profile that does not describe it.
|
|
492
|
+
*/
|
|
493
|
+
export function resolveKiroRequestProfileArn(
|
|
494
|
+
account?: Pick<KiroOAuthMetadata, "profileArn" | "authType">,
|
|
495
|
+
): string | undefined {
|
|
496
|
+
return resolveKiroRequestProfile(account).profileArn;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* The profileArn to send, together with WHY it was chosen.
|
|
501
|
+
*
|
|
502
|
+
* The request builder must decide the wire envelope from the same evaluation that produced the
|
|
503
|
+
* ARN. Re-deriving "is this Builder ID" from the account context alone would miss the accountless
|
|
504
|
+
* path, where the auth type comes from the locally imported credential instead: the fallback would
|
|
505
|
+
* be sent while the request was shaped as an enterprise IDE call, which is not a combination the
|
|
506
|
+
* vendor client ever produces.
|
|
507
|
+
*/
|
|
508
|
+
export function resolveKiroRequestProfile(
|
|
509
|
+
account?: Pick<KiroOAuthMetadata, "profileArn" | "authType">,
|
|
510
|
+
): { profileArn: string | undefined; builderIdFallback: boolean } {
|
|
511
|
+
const own = resolveKiroProfileArn(account);
|
|
512
|
+
if (own) return { profileArn: own, builderIdFallback: false };
|
|
513
|
+
const authType = account !== undefined
|
|
514
|
+
? account.authType
|
|
515
|
+
: readImportedKiroCredential()?.authType;
|
|
516
|
+
return authType === "aws_sso_oidc"
|
|
517
|
+
? { profileArn: KIRO_BUILDER_ID_SERVICE_PROFILE_ARN, builderIdFallback: true }
|
|
518
|
+
: { profileArn: undefined, builderIdFallback: false };
|
|
519
|
+
}
|
|
520
|
+
|
|
476
521
|
async function kiroTokenRefreshError(response: Response): Promise<KiroTokenRefreshError> {
|
|
477
522
|
let oauthError: string | undefined;
|
|
478
523
|
try {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Whether a login should open a browser on the machine running the proxy.
|
|
5
|
+
*
|
|
6
|
+
* Both login routes share this so the two surfaces cannot drift: a per-request
|
|
7
|
+
* `openBrowser` beats the persisted setting, and the persisted setting beats
|
|
8
|
+
* the historical default. It lives in `src/oauth/` rather than beside either
|
|
9
|
+
* route because `src/codex/auth-api.ts` and
|
|
10
|
+
* `src/server/management/oauth-account-routes.ts` both already import from
|
|
11
|
+
* here, so no new edge is added to the import graph.
|
|
12
|
+
*
|
|
13
|
+
* The default is the important part. An operator who upgrades and configures
|
|
14
|
+
* nothing must see exactly what they saw before, so only an explicit `false`
|
|
15
|
+
* declines — `undefined`, `true`, and a malformed value all open.
|
|
16
|
+
*
|
|
17
|
+
* A malformed request field is ignored rather than rejected on purpose: this is
|
|
18
|
+
* a display preference, and no login should fail because of one.
|
|
19
|
+
*/
|
|
20
|
+
export function shouldOpenBrowserForLogin(
|
|
21
|
+
requested: unknown,
|
|
22
|
+
config: Pick<OcxConfig, "oauthOpenBrowser">,
|
|
23
|
+
): boolean {
|
|
24
|
+
if (typeof requested === "boolean") return requested;
|
|
25
|
+
return config.oauthOpenBrowser !== false;
|
|
26
|
+
}
|
package/src/oauth/store.ts
CHANGED
|
@@ -591,17 +591,26 @@ export async function upsertCredentialByIdentity(
|
|
|
591
591
|
}, [provider, safe]);
|
|
592
592
|
}
|
|
593
593
|
|
|
594
|
-
/**
|
|
595
|
-
|
|
596
|
-
|
|
594
|
+
/**
|
|
595
|
+
* Remove the ACTIVE account; remaining accounts promote the first one.
|
|
596
|
+
*
|
|
597
|
+
* Returns what actually happened, which a caller cannot otherwise know. A read-then-remove
|
|
598
|
+
* preflight is not equivalent: `mutateStore` serializes mutations, so between a caller's
|
|
599
|
+
* `getAccountSet` check and its `removeCredential` call another process can remove the same
|
|
600
|
+
* account, and both callers would then report a removal that only one of them performed.
|
|
601
|
+
* Deciding inside the mutation is the only place the answer is true when it is returned.
|
|
602
|
+
*/
|
|
603
|
+
export async function removeCredential(provider: string): Promise<"removed" | "not-found"> {
|
|
604
|
+
return await mutateStore(store => {
|
|
597
605
|
const set = store[provider];
|
|
598
|
-
if (!set) return;
|
|
606
|
+
if (!set) return "not-found" as const;
|
|
599
607
|
set.accounts = set.accounts.filter(a => a.id !== set.activeAccountId);
|
|
600
608
|
if (set.accounts.length === 0) {
|
|
601
609
|
delete store[provider];
|
|
602
|
-
return;
|
|
610
|
+
return "removed" as const;
|
|
603
611
|
}
|
|
604
612
|
set.activeAccountId = set.accounts[0]!.id;
|
|
613
|
+
return "removed" as const;
|
|
605
614
|
}, [provider]);
|
|
606
615
|
}
|
|
607
616
|
|
package/src/oauth/types.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
/** Minimal OAuth types, ported from jawcode packages/ai/src/utils/oauth/types.ts. */
|
|
2
2
|
export type OAuthCredentialSource = "oauth" | "local-cli" | "credential-file" | "environment" | "manual";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* How the account authenticated. Mirrors `KiroAuthType` in `./kiro-credentials`, restated here so
|
|
6
|
+
* the credential-store types do not depend on the SQLite import module.
|
|
7
|
+
*
|
|
8
|
+
* `aws_sso_oidc` covers AWS Builder ID, which never issues an account-scoped CodeWhisperer
|
|
9
|
+
* profile ARN; the adapter needs that distinction to tell a Builder ID account apart from a
|
|
10
|
+
* `kiro_desktop` account whose profile import merely failed.
|
|
11
|
+
*/
|
|
12
|
+
export type KiroCredentialAuthType = "kiro_desktop" | "aws_sso_oidc";
|
|
13
|
+
|
|
4
14
|
/** Account-scoped Kiro data required for refresh and request routing. */
|
|
5
15
|
export interface KiroOAuthMetadata {
|
|
6
16
|
profileArn?: string;
|
|
@@ -8,6 +18,11 @@ export interface KiroOAuthMetadata {
|
|
|
8
18
|
apiRegion?: string;
|
|
9
19
|
clientId?: string;
|
|
10
20
|
clientSecret?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Non-secret routing signal. Derived from the presence of a device-registration client pair, so
|
|
23
|
+
* it stays accurate even though `clientId`/`clientSecret` never leave the credential store.
|
|
24
|
+
*/
|
|
25
|
+
authType?: KiroCredentialAuthType;
|
|
11
26
|
}
|
|
12
27
|
|
|
13
28
|
export type OAuthCredentials = {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { SUPPORTED_NATIVE_OPENAI_SLUGS } from "../codex/catalog/native-models";
|
|
2
|
+
import { redactSecretString } from "../lib/redact";
|
|
3
|
+
|
|
4
|
+
const RESERVED_OBJECT_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
5
|
+
|
|
6
|
+
function positiveSafeInteger(value: unknown): value is number {
|
|
7
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolve a client-facing soft compaction budget without changing any hard
|
|
12
|
+
* model limit. Configuration and measured input ceilings may only lower the
|
|
13
|
+
* default 90% envelope.
|
|
14
|
+
*/
|
|
15
|
+
export function clampAutoCompactTokenLimit(
|
|
16
|
+
contextWindow: number,
|
|
17
|
+
maxInputTokens?: number,
|
|
18
|
+
configuredLimit?: number,
|
|
19
|
+
): number {
|
|
20
|
+
const candidates = [Math.floor(contextWindow * 0.9), contextWindow];
|
|
21
|
+
if (positiveSafeInteger(maxInputTokens)) candidates.push(maxInputTokens);
|
|
22
|
+
if (positiveSafeInteger(configuredLimit)) candidates.push(configuredLimit);
|
|
23
|
+
return Math.min(...candidates);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type AutoCompactBudgetValidationOptions = Readonly<{
|
|
27
|
+
/** PATCH accepts null for whole-map and per-key deletion. */
|
|
28
|
+
allowTombstones?: boolean;
|
|
29
|
+
/** The canonical ChatGPT provider accepts only exact supported native ids. */
|
|
30
|
+
requireNativeIds?: boolean;
|
|
31
|
+
}>;
|
|
32
|
+
|
|
33
|
+
/** Shared config/load/management boundary for per-model soft budgets. */
|
|
34
|
+
export function modelAutoCompactTokenLimitsConfigError(
|
|
35
|
+
value: unknown,
|
|
36
|
+
options: AutoCompactBudgetValidationOptions = {},
|
|
37
|
+
): string | null {
|
|
38
|
+
const field = "modelAutoCompactTokenLimits";
|
|
39
|
+
if (value === undefined || (options.allowTombstones && value === null)) return null;
|
|
40
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
41
|
+
return `${field} must be a plain object${options.allowTombstones ? " or null" : ""}`;
|
|
42
|
+
}
|
|
43
|
+
const prototype = Object.getPrototypeOf(value);
|
|
44
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
45
|
+
return `${field} must be a plain object with own properties`;
|
|
46
|
+
}
|
|
47
|
+
for (const [modelId, entry] of Object.entries(value as Record<string, unknown>)) {
|
|
48
|
+
const safeModelId = JSON.stringify(redactSecretString(modelId));
|
|
49
|
+
if (!modelId.trim()) return `${field} keys must be nonblank model ids`;
|
|
50
|
+
if (RESERVED_OBJECT_KEYS.has(modelId)) {
|
|
51
|
+
return `${field} key ${safeModelId} is reserved`;
|
|
52
|
+
}
|
|
53
|
+
if (options.requireNativeIds
|
|
54
|
+
&& (modelId.includes("/") || !SUPPORTED_NATIVE_OPENAI_SLUGS.has(modelId))) {
|
|
55
|
+
return `${field} key ${safeModelId} must be an exact supported native model id`;
|
|
56
|
+
}
|
|
57
|
+
if (options.allowTombstones && entry === null) continue;
|
|
58
|
+
if (!positiveSafeInteger(entry)) {
|
|
59
|
+
return `${field}[${safeModelId}] must be a positive safe integer${
|
|
60
|
+
options.allowTombstones ? " or null" : ""
|
|
61
|
+
}`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|