/** * Prepares route-aware auth forwarding for auxiliary agent-runtime calls. * Callers supply an already loaded credential snapshot; this module never * resolves secrets or loads a provider runtime. */ import { resolveMergedModelProviderConfig } from "../../config/model-provider-config.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { ProviderResolveModelRoutesContext, ProviderRouteOverridePresence, } from "../../plugin-sdk/provider-model-types.js"; import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js"; import { isPendingOAuthRefreshFence } from "../auth-profiles/oauth-refresh-marker.js"; import { prependAuthProfilePin, resolveAuthProfileEligibility, resolveAuthProfileOrderWithMetadata, } from "../auth-profiles/order.js"; import { resolveStoredCredentialReadOnlyAvailability } from "../auth-profiles/read-only-availability.js"; import { createSelectedAuthProfileUnavailableError } from "../auth-profiles/selection-error.js"; import type { AuthProfileStore } from "../auth-profiles/types.js"; import { isProfileInCooldown } from "../auth-profiles/usage-state.js"; import { resolveProviderDirectAuthPlanningEvidence } from "../model-auth-env.js"; import { hasUsableCustomProviderApiKey, resolveProviderConfigSecretInput, resolveProviderEntryApiKeyProfileReference, shouldPreferExplicitConfigApiKeyAuth, } from "../model-auth-provider-config.js"; import { resolveModelProviderAuthConfig } from "../model-auth-provider-route.js"; import { resolveDefaultModelForAgent } from "../model-selection-config.js"; import { resolveOpenAIModelRoutes, selectOpenAIModelRouteAuth } from "../openai-model-routes.js"; import { buildProviderModelAuthDirectSource, buildProviderModelAuthSourcePlan, classifyProviderModelAuthSource, type ProviderModelAuthDirectSource, type ProviderModelAuthProfileSource, } from "../provider-model-auth-source-plan.js"; import { selectProviderModelAuthSources, resolveProviderModelRouteAuthRequirement, } from "../provider-model-route-auth.js"; import { buildAgentRuntimeAuthPlan } from "./auth.js"; import type { AgentRuntimeAuthPlan } from "./types.js"; type PrepareAgentRuntimeAuthPlanParams = { provider: string; modelId: string; modelApi?: string | null; modelBaseUrl?: unknown; requestTransportOverrides?: ProviderRouteOverridePresence; config?: OpenClawConfig; agentId?: string; routeIntent?: ProviderResolveModelRoutesContext["routeIntent"]; env?: NodeJS.ProcessEnv; agentDir?: string; workspaceDir?: string; metadataSnapshot?: PluginMetadataSnapshot; authProfileStore?: AuthProfileStore; sessionAuthProfileId?: string; sessionAuthProfileSource?: "auto" | "user" | "user-link"; allowAuthProfileFallback?: boolean; harnessId?: string; harnessRuntime?: string; harnessAuthBootstrap?: "harness"; allowHarnessAuthProfileForwarding?: boolean; allowTransientCooldownProbe?: boolean; resolveProviderPreferredProfileId?(context: { config?: OpenClawConfig; agentDir?: string; workspaceDir?: string; provider: string; modelId: string; preferredProfileId?: string; lockedProfileId?: string; profileOrder: string[]; authStore: AuthProfileStore; }): string | undefined; }; export type PreparedAgentRuntimeAuthAttempt = | { kind: "profile"; plan: AgentRuntimeAuthPlan; profileId: string; allowAuthProfileFallback?: never; requiresPriorProfileAttempt?: never; } | { kind: "direct"; plan: AgentRuntimeAuthPlan; profileId?: never; /** Direct lookup cannot re-enter automatic profile discovery. */ allowAuthProfileFallback: false; /** Fail closed when every prepared profile became cooldown-blocked before dispatch. */ requiresPriorProfileAttempt: boolean; } | { kind: "implicit"; plan: AgentRuntimeAuthPlan; profileId?: never; allowAuthProfileFallback?: never; requiresPriorProfileAttempt?: never; }; export type PreparedAgentRuntimeAuth = { plan: AgentRuntimeAuthPlan; /** Ordered physical attempts; every route/profile tuple was selected by this planner. */ attempts: readonly PreparedAgentRuntimeAuthAttempt[]; }; /** Prevents a direct fallback from bypassing a prepared profile tier. */ export function canRunPreparedAgentRuntimeAuthAttempt(params: { attempt: PreparedAgentRuntimeAuthAttempt; priorProfileAttempted: boolean; }): boolean { return ( params.attempt.kind !== "direct" || !params.attempt.requiresPriorProfileAttempt || params.priorProfileAttempted ); } /** Rechecks automatic cooldowns immediately before a prepared profile attempt. */ export function preparedAgentRuntimeProfileAttemptHasCandidate(params: { attempt: PreparedAgentRuntimeAuthAttempt; store: AuthProfileStore; modelId: string; }): boolean { if (params.attempt.kind !== "profile") { return false; } const profileIds = params.attempt.plan.forwardedAuthProfileCandidateIds ?? [ params.attempt.profileId, ]; return profileIds.some( (profileId) => !isProfileInCooldown(params.store, profileId, undefined, params.modelId), ); } /** True when a prepared auth tuple can be reused for this exact compaction target. */ export function agentRuntimeAuthPlanMatchesTarget( plan: AgentRuntimeAuthPlan, target: { provider: string; modelId: string }, ): boolean { const route = plan.modelRoute; const provider = route?.provider ?? plan.providerForAuth; const modelId = route?.modelId ?? plan.modelId; return ( modelId !== undefined && provider.trim().toLowerCase() === target.provider.trim().toLowerCase() && modelId === target.modelId ); } function resolveProfile( params: PrepareAgentRuntimeAuthPlanParams, profileId: string, options: { ignoreCooldown?: boolean } = {}, ): ProviderModelAuthProfileSource { const credential = params.authProfileStore?.profiles[profileId]; const configured = params.config?.auth?.profiles?.[profileId]; const availability = credential ? resolveStoredCredentialReadOnlyAvailability({ credential, cfg: params.config ?? {}, env: params.env ?? process.env, }) : undefined; const pendingOAuthRefresh = credential?.type === "oauth" && isPendingOAuthRefreshFence(credential); return { kind: "profile", profileId, provider: credential?.provider ?? configured?.provider, mode: credential?.type ?? configured?.mode, // Runtime materialization owns secret readiness; only proven-invalid facts are terminal here. readiness: availability === false && !pendingOAuthRefresh ? "unavailable" : "unknown", cooldown: !options.ignoreCooldown && params.authProfileStore && isProfileInCooldown(params.authProfileStore, profileId, undefined, params.modelId) ? "active" : "clear", }; } /** Applies terminal provider-entry credential policy before route selection. */ function resolvePreparedProviderEntryApiKeyProfileReference( params: PrepareAgentRuntimeAuthPlanParams & { store: AuthProfileStore }, ) { const reference = resolveProviderEntryApiKeyProfileReference({ cfg: params.config, authAliasLookupParams: params, provider: params.provider, store: params.store, }); if (reference.kind !== "profile") { return reference; } const eligibility = resolveAuthProfileEligibility({ cfg: params.config, authAliasLookupParams: params, store: params.store, provider: params.provider, profileId: reference.profileId, }); if (!eligibility.eligible) { throw new Error( `Per-entry apiKey profile "${reference.profileId}" has no usable credentials for ${params.provider}.`, ); } if (isProfileInCooldown(params.store, reference.profileId, undefined, params.modelId)) { throw new Error( `Auth profile "${reference.profileId}" is temporarily unavailable for ${params.provider}/${params.modelId}.`, ); } return reference; } /** Selects concrete provider routes and ordered credentials as one immutable preparation. */ export function prepareAgentRuntimeAuth( input: PrepareAgentRuntimeAuthPlanParams, ): PreparedAgentRuntimeAuth { const params = { ...input, config: resolveModelProviderAuthConfig(input) }; const requestedProfileId = params.sessionAuthProfileId?.trim() || undefined; const userPinnedProfileId = params.sessionAuthProfileSource === "user" || params.sessionAuthProfileSource === "user-link" ? requestedProfileId : undefined; const harnessOwnsOpenAIAuth = params.harnessId?.trim().toLowerCase() === "codex" || params.harnessRuntime?.trim().toLowerCase() === "codex"; const harnessAuthOwnerId = params.harnessId?.trim() || params.harnessRuntime?.trim(); const runtimeAuthOwner = harnessOwnsOpenAIAuth && params.harnessAuthBootstrap === "harness" && harnessAuthOwnerId ? { id: harnessAuthOwnerId } : undefined; const harnessAllowsAuthProfileForwarding = params.allowHarnessAuthProfileForwarding !== false; if (userPinnedProfileId && !harnessAllowsAuthProfileForwarding) { throw new Error( `Auth profile "${userPinnedProfileId}" cannot be forwarded to the selected agent harness. Configure that harness's native account instead.`, ); } const store = params.authProfileStore; const authProfileSelectionProvider = harnessOwnsOpenAIAuth ? "openai" : params.provider; if (userPinnedProfileId) { const eligibility = store ? resolveAuthProfileEligibility({ cfg: params.config, authAliasLookupParams: params, store, provider: authProfileSelectionProvider, profileId: userPinnedProfileId, includePendingOAuthRefresh: true, }) : { eligible: false }; if (!eligibility.eligible) { if ( !store?.profiles[userPinnedProfileId] && params.config?.auth?.profiles?.[userPinnedProfileId]?.mode !== "aws-sdk" ) { throw createSelectedAuthProfileUnavailableError({ profileId: userPinnedProfileId, provider: authProfileSelectionProvider, modelId: params.modelId, }); } throw new Error( `Auth profile "${userPinnedProfileId}" is not configured for ${authProfileSelectionProvider}.`, ); } } const configuredProvider = resolveMergedModelProviderConfig(params.config, params.provider); const configuredAuthMode = userPinnedProfileId || !harnessAllowsAuthProfileForwarding ? undefined : configuredProvider?.auth; const configuredAwsSdkAuth = configuredAuthMode === "aws-sdk"; const providerApiKeySecretRef = harnessAllowsAuthProfileForwarding ? resolveProviderConfigSecretInput(params.config, params.provider).ref : undefined; const providerHasApiKeySecretRef = Boolean(providerApiKeySecretRef); const providerBinding = harnessAllowsAuthProfileForwarding && !userPinnedProfileId && store && !configuredAwsSdkAuth ? resolvePreparedProviderEntryApiKeyProfileReference({ ...params, store, }) : { kind: "none" as const }; if (providerBinding.kind === "profile-incompatible") { throw new Error( `Per-entry apiKey "${providerBinding.profileId}" is not a compatible bearer profile for ${params.provider}.`, ); } const boundProfileId = providerBinding.kind === "profile" ? providerBinding.profileId : undefined; const providerHasUsableMarker = providerBinding.kind === "marker" && hasUsableCustomProviderApiKey(params.config, params.provider, params.env); const providerHasDirectMaterial = !configuredAwsSdkAuth && (providerBinding.kind === "literal" || providerHasUsableMarker || providerHasApiKeySecretRef); const explicitConfigApiKeyAuth = shouldPreferExplicitConfigApiKeyAuth( params.config, params.provider, ); const providerBindingSuppressesProfiles = (providerBinding.kind === "literal" && explicitConfigApiKeyAuth) || providerHasUsableMarker || providerHasApiKeySecretRef; const providerBindingNeedsNonProfileFallback = providerHasDirectMaterial && !providerBindingSuppressesProfiles; // Explicit auth owns the physical route; apiKey is only its bearer material. const selectedConfiguredAuthMode = configuredAuthMode ?? (providerHasDirectMaterial ? "api-key" : undefined); const selectedProfileId = boundProfileId ?? (params.allowAuthProfileFallback === false ? userPinnedProfileId : undefined); const resolvedAutomaticOrder = !harnessAllowsAuthProfileForwarding || selectedProfileId || providerBindingSuppressesProfiles || configuredAwsSdkAuth || !store ? { profileIds: selectedProfileId ? [selectedProfileId] : [], hasExplicitOrder: false, } : resolveAuthProfileOrderWithMetadata({ cfg: params.config, authAliasLookupParams: params, store, provider: authProfileSelectionProvider, preferredProfile: requestedProfileId, forModel: params.modelId, readinessMode: "read-only", includePendingOAuthRefresh: true, }); const automaticOrderResolution = prependAuthProfilePin( resolvedAutomaticOrder, userPinnedProfileId, ); const providerPreferredProfileId = harnessAllowsAuthProfileForwarding && !selectedProfileId && !userPinnedProfileId && !providerBindingSuppressesProfiles && !configuredAwsSdkAuth && store ? params.resolveProviderPreferredProfileId?.({ config: params.config, agentDir: params.agentDir, workspaceDir: params.workspaceDir, provider: params.provider, modelId: params.modelId, preferredProfileId: requestedProfileId, lockedProfileId: undefined, profileOrder: automaticOrderResolution.profileIds, authStore: store, }) : undefined; const resolvedOrderedProfileIds = providerPreferredProfileId && automaticOrderResolution.profileIds.includes(providerPreferredProfileId) ? [ providerPreferredProfileId, ...automaticOrderResolution.profileIds.filter( (profileId) => profileId !== providerPreferredProfileId, ), ] : automaticOrderResolution.profileIds; const directSource = ( mode: string | undefined, evidence: ProviderModelAuthDirectSource["evidence"] = providerBinding.kind === "marker" && providerHasUsableMarker ? providerBinding.evidence : providerApiKeySecretRef?.source === "env" ? "environment" : "provider-config", availability?: boolean, authorization: ProviderModelAuthDirectSource["authorization"] = "declared", ) => buildProviderModelAuthDirectSource({ mode, evidence, availability, authorization }); const directPlanningCandidate = harnessAllowsAuthProfileForwarding ? resolveProviderDirectAuthPlanningEvidence( authProfileSelectionProvider, params.env ?? process.env, { config: params.config, workspaceDir: params.workspaceDir, metadataSnapshot: params.metadataSnapshot, }, ) : null; // OpenAI native account discovery is harness-owned synthetic auth, not a // bearer credential for an OpenClaw request route. const directPlanningEvidence = directPlanningCandidate?.kind === "setup-provider" && authProfileSelectionProvider.trim().toLowerCase() === "openai" ? null : directPlanningCandidate; const directPlanningMode = directPlanningEvidence ? (configuredAuthMode ?? directPlanningEvidence.mode) : undefined; // Provenance ("where was it found") is not authorization ("may it be used // here"). A credential found in the environment is still *declared* when the // provider entry points at it — a literal apiKey, a `${VAR}` marker, or a // SecretRef naming a canonical variable. Only a credential that nothing in // config references is ambient, and only ambient credentials are restricted. const fallbackIsAmbientCredential = directPlanningEvidence?.kind === "environment" && !providerHasDirectMaterial; const fallbackDirectSource = directPlanningMode ? directSource( directPlanningMode, directPlanningEvidence?.kind === "environment" ? "environment" : "runtime", directPlanningEvidence?.kind === "environment" ? true : undefined, fallbackIsAmbientCredential ? "ambient" : "declared", ) : providerBindingNeedsNonProfileFallback ? directSource(selectedConfiguredAuthMode) : undefined; const automaticRouteAuthMode = fallbackDirectSource && !providerBindingSuppressesProfiles && !configuredAuthMode ? undefined : selectedConfiguredAuthMode; const ownership = selectedProfileId ? { reason: selectedProfileId === userPinnedProfileId ? ("runtime-binding" as const) : ("provider-binding" as const), source: resolveProfile(params, selectedProfileId, { ignoreCooldown: true }), } : configuredAwsSdkAuth ? { reason: "configured-auth" as const, source: directSource("aws-sdk", "aws-sdk"), } : providerBindingSuppressesProfiles ? { reason: "configured-auth" as const, source: directSource(selectedConfiguredAuthMode), } : undefined; const sourcePlan = buildProviderModelAuthSourcePlan({ ...(ownership ? { ownership } : {}), profiles: resolvedOrderedProfileIds.map((profileId) => resolveProfile(params, profileId)), ...(userPinnedProfileId || providerPreferredProfileId ? { preferredProfileId: userPinnedProfileId ?? providerPreferredProfileId } : {}), explicitOrder: automaticOrderResolution.hasExplicitOrder, preserveProfilePriority: Boolean(userPinnedProfileId), ...(fallbackDirectSource ? { fallback: fallbackDirectSource } : {}), allowCooldown: params.allowTransientCooldownProbe, }); const resolution = resolveOpenAIModelRoutes({ provider: params.provider, modelId: params.modelId, api: params.modelApi, baseUrl: params.modelBaseUrl, config: params.config, agentId: params.agentId, primaryModel: !params.routeIntent && params.config ? resolveDefaultModelForAgent({ cfg: params.config, agentId: params.agentId, allowManifestNormalization: false, allowPluginNormalization: false, }) : undefined, resolveProfileAuthMode: (profileId) => params.authProfileStore?.profiles[profileId]?.type, routeIntent: params.routeIntent, pinnedAuthRequirement: resolveProviderModelRouteAuthRequirement( sourcePlan.kind === "required" ? sourcePlan.source.mode : (sourcePlan.orderedProfiles.find((source) => source.profileId === userPinnedProfileId) ?.mode ?? configuredAuthMode), ), env: params.env, requestTransportOverrides: params.requestTransportOverrides, }); if (!resolution || resolution.kind === "indeterminate") { const sourceDecision = selectProviderModelAuthSources({ provider: authProfileSelectionProvider, plan: sourcePlan, }); if (sourceDecision.kind === "rejected") { if (sourceDecision.reason === "all-cooldown" && sourceDecision.source) { throw new Error( `Auth profile "${sourceDecision.source.profileId}" is temporarily unavailable for ${params.provider}/${params.modelId}.`, ); } throw new Error(sourceDecision.message); } const buildGenericPlan = ( attempt: (typeof sourceDecision.attempts)[number] | undefined, candidateIndex: number, ) => { const profile = attempt?.kind === "profile" ? attempt.source : undefined; const candidateIds = sourceDecision.attempts .slice(candidateIndex) .flatMap((candidate) => (candidate.kind === "profile" ? [candidate.source.profileId] : [])); return buildAgentRuntimeAuthPlan({ provider: params.provider, modelId: params.modelId, authProfileProvider: profile?.provider, authProfileMode: profile?.mode ?? (attempt?.kind === "direct" ? attempt.source.mode : selectedConfiguredAuthMode), sessionAuthProfileId: profile?.profileId, sessionAuthProfileSource: profile ? profile.profileId === userPinnedProfileId ? "user" : "auto" : undefined, sessionAuthProfileCandidateIds: candidateIds.length > 0 ? candidateIds : undefined, credentialSource: attempt ? classifyProviderModelAuthSource(attempt.source) : { kind: "none" }, config: params.config, env: params.env, workspaceDir: params.workspaceDir, metadataSnapshot: params.metadataSnapshot, harnessId: params.harnessId, harnessRuntime: params.harnessRuntime, allowHarnessAuthProfileForwarding: harnessAllowsAuthProfileForwarding, }); }; const attempts: PreparedAgentRuntimeAuthAttempt[] = sourceDecision.attempts.map( (attempt, index) => { const plan = buildGenericPlan(attempt, index); return attempt.kind === "profile" ? { kind: "profile", plan, profileId: attempt.source.profileId } : { kind: "direct", plan, allowAuthProfileFallback: attempt.allowAuthProfileFallback, requiresPriorProfileAttempt: sourceDecision.attempts .slice(0, index) .some((candidate) => candidate.kind === "profile"), }; }, ); const plan = attempts[0]?.plan ?? buildGenericPlan(undefined, 0); if ( selectedProfileId && harnessOwnsOpenAIAuth && plan.forwardedAuthProfileId !== selectedProfileId ) { throw new Error( `Auth profile "${selectedProfileId}" cannot be forwarded to the codex runtime.`, ); } return { plan, attempts: attempts.length > 0 ? attempts : [{ kind: "implicit", plan }], }; } if (resolution.kind === "incompatible") { throw new Error(resolution.message); } const toPreparedRoute = (route: (typeof resolution.routes)[number]) => ({ provider: params.provider, modelId: params.modelId, api: route.api, baseUrl: route.baseUrl, authRequirement: route.authRequirement, requestTransportOverrides: route.requestTransportOverrides, runtimePolicy: route.runtimePolicy, }); const routeAuthDecision = selectOpenAIModelRouteAuth({ resolution, sourcePlan, configuredAuthMode: automaticRouteAuthMode, ...(runtimeAuthOwner ? { runtimeAuthOwner } : {}), ...(runtimeAuthOwner && configuredProvider === undefined ? { allowNativeAuthOnSingleRoute: true } : {}), }); if (routeAuthDecision.kind === "deferred") { const plan = buildAgentRuntimeAuthPlan({ provider: params.provider, modelId: params.modelId, config: params.config, env: params.env, workspaceDir: params.workspaceDir, metadataSnapshot: params.metadataSnapshot, harnessId: params.harnessId, harnessRuntime: params.harnessRuntime, allowHarnessAuthProfileForwarding: harnessAllowsAuthProfileForwarding, deferredRouteSupport: routeAuthDecision.routeSupport, }); return { plan, attempts: [{ kind: "implicit", plan }] }; } if (routeAuthDecision.kind !== "selected") { if ( routeAuthDecision.kind === "rejected" && routeAuthDecision.reason === "all-cooldown" && routeAuthDecision.source ) { throw new Error( `Auth profile "${routeAuthDecision.source.profileId}" is temporarily unavailable for ${params.provider}/${params.modelId}.`, ); } throw new Error(routeAuthDecision.message); } const buildRoutedPlan = (attempt: (typeof routeAuthDecision.attempts)[number] | undefined) => { const profile = attempt?.kind === "profile" ? attempt.source : undefined; const route = attempt?.route ?? routeAuthDecision.selection.route; return buildAgentRuntimeAuthPlan({ provider: params.provider, modelId: params.modelId, authProfileProvider: profile?.provider, authProfileMode: profile?.mode ?? (attempt?.kind === "direct" ? attempt.source.mode : selectedConfiguredAuthMode), sessionAuthProfileId: profile?.profileId, sessionAuthProfileSource: profile ? profile.profileId === userPinnedProfileId ? "user" : "auto" : undefined, sessionAuthProfileCandidateIds: attempt?.kind === "profile" ? [...attempt.sameRouteProfileIds] : undefined, credentialSource: attempt ? classifyProviderModelAuthSource(attempt.source) : { kind: "none" }, modelRoute: toPreparedRoute(route), config: params.config, env: params.env, workspaceDir: params.workspaceDir, metadataSnapshot: params.metadataSnapshot, harnessId: params.harnessId, harnessRuntime: params.harnessRuntime, allowHarnessAuthProfileForwarding: harnessAllowsAuthProfileForwarding, }); }; const attempts: PreparedAgentRuntimeAuthAttempt[] = routeAuthDecision.attempts.map( (attempt, index) => { const plan = buildRoutedPlan(attempt); return attempt.kind === "profile" ? { kind: "profile", plan, profileId: attempt.source.profileId } : { kind: "direct", plan, allowAuthProfileFallback: attempt.allowAuthProfileFallback, requiresPriorProfileAttempt: routeAuthDecision.attempts .slice(0, index) .some((candidate) => candidate.kind === "profile"), }; }, ); const plan = attempts[0]?.plan ?? buildRoutedPlan(undefined); for (const attempt of attempts) { if ( attempt.profileId && harnessOwnsOpenAIAuth && attempt.plan.forwardedAuthProfileId !== attempt.profileId ) { throw new Error( `Auth profile "${attempt.profileId}" cannot be forwarded to the codex runtime.`, ); } } return { plan, attempts: attempts.length > 0 ? attempts : [{ kind: "implicit", plan }], }; }