diff --git a/src/agents/auth-profiles/effective-oauth.ts b/src/agents/auth-profiles/effective-oauth.ts index 3b3958e23c3..534a50d397d 100644 --- a/src/agents/auth-profiles/effective-oauth.ts +++ b/src/agents/auth-profiles/effective-oauth.ts @@ -1,4 +1,4 @@ -import { readExternalCliBootstrapCredential } from "./external-cli-sync.js"; +import { readManagedExternalCliCredential } from "./external-cli-sync.js"; import { resolveEffectiveOAuthCredential as resolveManagedOAuthCredential } from "./oauth-manager.js"; import type { OAuthCredential } from "./types.js"; @@ -10,7 +10,7 @@ export function resolveEffectiveOAuthCredential(params: { profileId: params.profileId, credential: params.credential, readBootstrapCredential: ({ profileId, credential }) => - readExternalCliBootstrapCredential({ + readManagedExternalCliCredential({ profileId, credential, }), diff --git a/src/agents/auth-profiles/external-auth.ts b/src/agents/auth-profiles/external-auth.ts index bd46682f9ce..14a85afac0b 100644 --- a/src/agents/auth-profiles/external-auth.ts +++ b/src/agents/auth-profiles/external-auth.ts @@ -1,6 +1,11 @@ import type { ProviderExternalAuthProfile } from "../../plugins/provider-external-auth.types.js"; import { resolveExternalAuthProfilesWithPlugins } from "../../plugins/provider-runtime.js"; import * as externalCliSync from "./external-cli-sync.js"; +import { + overlayRuntimeExternalOAuthProfiles, + shouldPersistRuntimeExternalOAuthProfile, + type RuntimeExternalOAuthProfile, +} from "./oauth-manager.js"; import type { AuthProfileStore, OAuthCredential } from "./types.js"; type ExternalAuthProfileMap = Map; @@ -67,19 +72,17 @@ function resolveExternalAuthProfileMap(params: { return resolved; } -function oauthCredentialMatches(a: OAuthCredential, b: OAuthCredential): boolean { - return ( - a.type === b.type && - a.provider === b.provider && - a.access === b.access && - a.refresh === b.refresh && - a.expires === b.expires && - a.clientId === b.clientId && - a.email === b.email && - a.displayName === b.displayName && - a.enterpriseUrl === b.enterpriseUrl && - a.projectId === b.projectId && - a.accountId === b.accountId +function listRuntimeExternalAuthProfiles(params: { + store: AuthProfileStore; + agentDir?: string; + env?: NodeJS.ProcessEnv; +}): RuntimeExternalOAuthProfile[] { + return Array.from( + resolveExternalAuthProfileMap({ + store: params.store, + agentDir: params.agentDir, + env: params.env, + }).values(), ); } @@ -87,20 +90,12 @@ export function overlayExternalAuthProfiles( store: AuthProfileStore, params?: { agentDir?: string; env?: NodeJS.ProcessEnv }, ): AuthProfileStore { - const profiles = resolveExternalAuthProfileMap({ + const profiles = listRuntimeExternalAuthProfiles({ store, agentDir: params?.agentDir, env: params?.env, }); - if (profiles.size === 0) { - return store; - } - - const next = structuredClone(store); - for (const [profileId, profile] of profiles) { - next.profiles[profileId] = profile.credential; - } - return next; + return overlayRuntimeExternalOAuthProfiles(store, profiles); } export function shouldPersistExternalAuthProfile(params: { @@ -110,15 +105,16 @@ export function shouldPersistExternalAuthProfile(params: { agentDir?: string; env?: NodeJS.ProcessEnv; }): boolean { - const external = resolveExternalAuthProfileMap({ + const profiles = listRuntimeExternalAuthProfiles({ store: params.store, agentDir: params.agentDir, env: params.env, - }).get(params.profileId); - if (!external || external.persistence === "persisted") { - return true; - } - return !oauthCredentialMatches(external.credential, params.credential); + }); + return shouldPersistRuntimeExternalOAuthProfile({ + profileId: params.profileId, + credential: params.credential, + profiles, + }); } // Compat aliases while file/function naming catches up. diff --git a/src/agents/auth-profiles/external-cli-sync.ts b/src/agents/auth-profiles/external-cli-sync.ts index 594ec4fc856..ab1104eb7cd 100644 --- a/src/agents/auth-profiles/external-cli-sync.ts +++ b/src/agents/auth-profiles/external-cli-sync.ts @@ -8,9 +8,21 @@ import { OPENAI_CODEX_DEFAULT_PROFILE_ID, } from "./constants.js"; import { log } from "./constants.js"; -import { hasUsableOAuthCredential as hasUsableOAuthCredentialShared } from "./credential-state.js"; +import { + areOAuthCredentialsEquivalent, + hasUsableOAuthCredential, + shouldBootstrapFromExternalCliCredential, + shouldReplaceStoredOAuthCredential, +} from "./oauth-manager.js"; import type { AuthProfileStore, OAuthCredential } from "./types.js"; +export { + areOAuthCredentialsEquivalent, + hasUsableOAuthCredential, + shouldBootstrapFromExternalCliCredential, + shouldReplaceStoredOAuthCredential, +} from "./oauth-manager.js"; + export type ExternalCliResolvedProfile = { profileId: string; credential: OAuthCredential; @@ -22,25 +34,6 @@ type ExternalCliSyncProvider = { readCredentials: () => OAuthCredential | null; }; -export function areOAuthCredentialsEquivalent( - a: OAuthCredential | undefined, - b: OAuthCredential, -): boolean { - if (!a || a.type !== "oauth") { - return false; - } - return ( - a.provider === b.provider && - a.access === b.access && - a.refresh === b.refresh && - a.expires === b.expires && - a.email === b.email && - a.enterpriseUrl === b.enterpriseUrl && - a.projectId === b.projectId && - a.accountId === b.accountId - ); -} - function normalizeAuthIdentityToken(value: string | undefined): string | undefined { const trimmed = value?.trim(); return trimmed ? trimmed : undefined; @@ -81,53 +74,6 @@ export function isSafeToUseExternalCliCredential( return true; } -function hasNewerStoredOAuthCredential( - existing: OAuthCredential | undefined, - incoming: OAuthCredential, -): boolean { - return Boolean( - existing && - existing.provider === incoming.provider && - Number.isFinite(existing.expires) && - (!Number.isFinite(incoming.expires) || existing.expires > incoming.expires), - ); -} - -export function shouldReplaceStoredOAuthCredential( - existing: OAuthCredential | undefined, - incoming: OAuthCredential, -): boolean { - if (!existing || existing.type !== "oauth") { - return true; - } - if (areOAuthCredentialsEquivalent(existing, incoming)) { - return false; - } - return !hasNewerStoredOAuthCredential(existing, incoming); -} - -export function hasUsableOAuthCredential( - credential: OAuthCredential | undefined, - now = Date.now(), -): boolean { - return hasUsableOAuthCredentialShared(credential, { now }); -} - -export function shouldBootstrapFromExternalCliCredential(params: { - existing: OAuthCredential | undefined; - imported: OAuthCredential; - now?: number; -}): boolean { - const now = params.now ?? Date.now(); - if (!isSafeToUseExternalCliCredential(params.existing, params.imported)) { - return false; - } - if (hasUsableOAuthCredential(params.existing, now)) { - return false; - } - return hasUsableOAuthCredential(params.imported, now); -} - const EXTERNAL_CLI_SYNC_PROVIDERS: ExternalCliSyncProvider[] = [ { profileId: MINIMAX_CLI_PROFILE_ID, diff --git a/src/agents/auth-profiles/oauth-manager.ts b/src/agents/auth-profiles/oauth-manager.ts index f55977b6a70..3d5d4bab611 100644 --- a/src/agents/auth-profiles/oauth-manager.ts +++ b/src/agents/auth-profiles/oauth-manager.ts @@ -59,7 +59,13 @@ export class OAuthManagerRefreshError extends Error { } } -function areOAuthCredentialsEquivalent( +export type RuntimeExternalOAuthProfile = { + profileId: string; + credential: OAuthCredential; + persistence?: "runtime-only" | "persisted"; +}; + +export function areOAuthCredentialsEquivalent( a: OAuthCredential | undefined, b: OAuthCredential, ): boolean { @@ -90,7 +96,7 @@ function hasNewerStoredOAuthCredential( ); } -function shouldReplaceStoredOAuthCredential( +export function shouldReplaceStoredOAuthCredential( existing: OAuthCredential | undefined, incoming: OAuthCredential, ): boolean { @@ -103,7 +109,7 @@ function shouldReplaceStoredOAuthCredential( return !hasNewerStoredOAuthCredential(existing, incoming); } -function hasUsableOAuthCredential( +export function hasUsableOAuthCredential( credential: OAuthCredential | undefined, now = Date.now(), ): boolean { @@ -116,7 +122,7 @@ function hasUsableOAuthCredential( return resolveTokenExpiryState(credential.expires, now) === "valid"; } -function shouldBootstrapFromExternalCliCredential(params: { +export function shouldBootstrapFromExternalCliCredential(params: { existing: OAuthCredential | undefined; imported: OAuthCredential; now?: number; @@ -128,6 +134,38 @@ function shouldBootstrapFromExternalCliCredential(params: { return hasUsableOAuthCredential(params.imported, now); } +export function overlayRuntimeExternalOAuthProfiles( + store: AuthProfileStore, + profiles: Iterable, +): AuthProfileStore { + const externalProfiles = Array.from(profiles); + if (externalProfiles.length === 0) { + return store; + } + const next = structuredClone(store); + for (const profile of externalProfiles) { + next.profiles[profile.profileId] = profile.credential; + } + return next; +} + +export function shouldPersistRuntimeExternalOAuthProfile(params: { + profileId: string; + credential: OAuthCredential; + profiles: Iterable; +}): boolean { + for (const profile of params.profiles) { + if (profile.profileId !== params.profileId) { + continue; + } + if (profile.persistence === "persisted") { + return true; + } + return !areOAuthCredentialsEquivalent(profile.credential, params.credential); + } + return true; +} + function hasOAuthCredentialChanged( previous: Pick, current: Pick,