11import { hashRuntimeConfigValue } from "../config/runtime-snapshot.js" ;
22import type { OpenClawConfig } from "../config/types.openclaw.js" ;
3+ import {
4+ listAgentIds ,
5+ resolveAgentDir ,
6+ resolveAgentWorkspaceDir ,
7+ resolveDefaultAgentId ,
8+ } from "./agent-scope-config.js" ;
39import {
410 externalCliDiscoveryForProviderAuth ,
511 externalCliDiscoveryForProviders ,
@@ -20,24 +26,43 @@ import { resolveDefaultAgentWorkspaceDir } from "./workspace.js";
2026// discovery and external-CLI probing on the hot path.
2127
2228type PreparedProviderAuthState = {
29+ agentId : string ;
2330 configFingerprint : string ;
24- workspaceDir : string ;
25- preparedAtMs : number ;
2631 providers : ReadonlyMap < string , boolean > ;
2732} ;
2833
29- const PREPARED_PROVIDER_AUTH_STATE_TTL_MS = 10_000 ;
30- let currentProviderAuthState : PreparedProviderAuthState | null = null ;
34+ // One entry per configured agent, keyed by agentId. Populated by
35+ // warmCurrentProviderAuthState at gateway startup / on reload; consulted by
36+ // hasAuthForModelProvider on every model-listing call.
37+ let currentProviderAuthStates : ReadonlyMap < string , PreparedProviderAuthState > | null = null ;
3138const configFingerprintCache = new WeakMap < OpenClawConfig , string > ( ) ;
3239// Generation counter guards against an in-flight warm publishing stale
3340// state after a subsequent warm or clear has invalidated it.
3441let currentProviderAuthStateGeneration = 0 ;
3542
3643export function clearCurrentProviderAuthState ( ) : void {
37- currentProviderAuthState = null ;
44+ currentProviderAuthStates = null ;
3845 currentProviderAuthStateGeneration += 1 ;
3946}
4047
48+ function resolvePreparedStateForCaller ( params : {
49+ states : ReadonlyMap < string , PreparedProviderAuthState > | null ;
50+ cfg : OpenClawConfig | undefined ;
51+ callerAgentId : string | undefined ;
52+ } ) : PreparedProviderAuthState | null {
53+ if ( ! params . states ) {
54+ return null ;
55+ }
56+ if ( params . callerAgentId !== undefined ) {
57+ return params . states . get ( params . callerAgentId ) ?? null ;
58+ }
59+ // Caller didn't pass agentId: treat as a query against the default agent.
60+ if ( ! params . cfg ) {
61+ return null ;
62+ }
63+ return params . states . get ( resolveDefaultAgentId ( params . cfg ) ) ?? null ;
64+ }
65+
4166function resolveProviderAuthConfigFingerprint ( cfg : OpenClawConfig | undefined ) : string | null {
4267 if ( ! cfg ) {
4368 return null ;
@@ -55,33 +80,41 @@ export function hasAuthForModelProvider(params: {
5580 provider : string ;
5681 cfg ?: OpenClawConfig ;
5782 workspaceDir ?: string ;
58- agentDir ?: string ;
83+ agentId ?: string ;
5984 env ?: NodeJS . ProcessEnv ;
6085 store ?: AuthProfileStore ;
6186 allowPluginSyntheticAuth ?: boolean ;
6287 discoverExternalCliAuth ?: boolean ;
6388} ) : boolean {
6489 const provider = normalizeProviderId ( params . provider ) ;
65- // The prepared map is built by warmCurrentProviderAuthState with broad
66- // auth discovery (external CLI + plugin synthetic auth enabled) and the
67- // default-agent workspace dir. Only consult it when the caller's full
68- // auth context matches; otherwise fall through to compute so callers
69- // that narrow the scope — e.g. gateway `models.list` with
70- // `runtimeAuthDiscovery: false`, or per-agent picker calls that pass a
71- // non-default workspaceDir — get the answer they asked for.
72- const preparedState = currentProviderAuthState ;
90+ // The prepared map is built by warmCurrentProviderAuthState — one entry per
91+ // configured agent, keyed by agentId. Only consult it when the caller's
92+ // full auth context matches the warmed scope; otherwise fall through to
93+ // compute so callers that narrow the scope — e.g. gateway `models.list`
94+ // with `runtimeAuthDiscovery: false`, or callers with a non-warmed
95+ // workspaceDir — get the answer they asked for.
96+ const preparedStates = currentProviderAuthStates ;
7397 const workspaceDir = params . workspaceDir ?? resolveDefaultAgentWorkspaceDir ( ) ;
7498 const configFingerprint = resolveProviderAuthConfigFingerprint ( params . cfg ) ;
75- const preparedStateFresh =
76- preparedState !== null &&
77- Date . now ( ) - preparedState . preparedAtMs <= PREPARED_PROVIDER_AUTH_STATE_TTL_MS ;
99+ const preparedState = resolvePreparedStateForCaller ( {
100+ states : preparedStates ,
101+ cfg : params . cfg ,
102+ callerAgentId : params . agentId ,
103+ } ) ;
104+ // workspaceDir is a pure function of (cfg, agentId), so we recompute the
105+ // warmer's expected value at read time rather than storing it. Caller can
106+ // still override workspaceDir explicitly — that forces a mismatch and
107+ // falls through to the compute path.
108+ const expectedWorkspaceDir =
109+ preparedState !== null && params . cfg
110+ ? resolveAgentWorkspaceDir ( params . cfg , preparedState . agentId )
111+ : null ;
78112 const matchesWarmedScope =
79- preparedStateFresh &&
113+ preparedState !== null &&
80114 configFingerprint === preparedState . configFingerprint &&
81- workspaceDir === preparedState . workspaceDir &&
115+ workspaceDir === expectedWorkspaceDir &&
82116 params . discoverExternalCliAuth !== false &&
83117 params . allowPluginSyntheticAuth !== false &&
84- params . agentDir === undefined &&
85118 params . env === undefined &&
86119 params . store === undefined ;
87120 if ( matchesWarmedScope ) {
@@ -101,13 +134,15 @@ export function hasAuthForModelProvider(params: {
101134 ) {
102135 return true ;
103136 }
137+ const slowPathAgentDir =
138+ params . agentId && params . cfg ? resolveAgentDir ( params . cfg , params . agentId ) : undefined ;
104139 const store =
105140 params . store ??
106141 ( params . discoverExternalCliAuth === false
107- ? ensureAuthProfileStoreWithoutExternalProfiles ( params . agentDir , {
142+ ? ensureAuthProfileStoreWithoutExternalProfiles ( slowPathAgentDir , {
108143 allowKeychainPrompt : false ,
109144 } )
110- : ensureAuthProfileStore ( params . agentDir , {
145+ : ensureAuthProfileStore ( slowPathAgentDir , {
111146 externalCli : externalCliDiscoveryForProviderAuth ( { cfg : params . cfg , provider } ) ,
112147 } ) ) ;
113148 if ( listProfilesForProvider ( store , provider ) . length > 0 ) {
@@ -119,7 +154,7 @@ export function hasAuthForModelProvider(params: {
119154export function createProviderAuthChecker ( params : {
120155 cfg ?: OpenClawConfig ;
121156 workspaceDir ?: string ;
122- agentDir ?: string ;
157+ agentId ?: string ;
123158 env ?: NodeJS . ProcessEnv ;
124159 allowPluginSyntheticAuth ?: boolean ;
125160 discoverExternalCliAuth ?: boolean ;
@@ -135,7 +170,7 @@ export function createProviderAuthChecker(params: {
135170 provider : key ,
136171 cfg : params . cfg ,
137172 workspaceDir : params . workspaceDir ,
138- agentDir : params . agentDir ,
173+ agentId : params . agentId ,
139174 env : params . env ,
140175 allowPluginSyntheticAuth : params . allowPluginSyntheticAuth ,
141176 discoverExternalCliAuth : params . discoverExternalCliAuth ,
@@ -155,35 +190,45 @@ export async function warmCurrentProviderAuthState(cfg: OpenClawConfig): Promise
155190 for ( const entry of catalog ) {
156191 providers . add ( normalizeProviderId ( entry . provider ) ) ;
157192 }
158- const workspaceDir = resolveDefaultAgentWorkspaceDir ( ) ;
159- // One AuthProfileStore scoped to every candidate provider; without this the
160- // per-provider externalCli discovery rebuilds the store ~N times.
161- const store = ensureAuthProfileStore ( undefined , {
162- config : cfg ,
163- externalCli : externalCliDiscoveryForProviders ( {
164- cfg,
165- providers : [ ...providers ] ,
166- } ) ,
167- } ) ;
168- const state = new Map < string , boolean > ( ) ;
169- for ( const provider of providers ) {
170- const value = hasAuthForModelProvider ( {
171- provider,
172- cfg,
173- workspaceDir,
174- store,
193+ const providerList = [ ...providers ] ;
194+ const configFingerprint = resolveProviderAuthConfigFingerprint ( cfg ) ?? "" ;
195+ const states = new Map < string , PreparedProviderAuthState > ( ) ;
196+ // Warm one entry per configured agent so callers hit the prepared map for
197+ // any agentId. The catalog above is shared across agents; the per-agent
198+ // work is the auth-discovery sweep against that agent's store.
199+ for ( const agentId of listAgentIds ( cfg ) ) {
200+ const workspaceDir = resolveAgentWorkspaceDir ( cfg , agentId ) ;
201+ const agentDir = resolveAgentDir ( cfg , agentId ) ;
202+ // One AuthProfileStore scoped to every candidate provider; without this
203+ // the per-provider externalCli discovery rebuilds the store ~N times.
204+ const store = ensureAuthProfileStore ( agentDir , {
205+ config : cfg ,
206+ externalCli : externalCliDiscoveryForProviders ( {
207+ cfg,
208+ providers : providerList ,
209+ } ) ,
210+ } ) ;
211+ const state = new Map < string , boolean > ( ) ;
212+ for ( const provider of providers ) {
213+ const value = hasAuthForModelProvider ( {
214+ provider,
215+ cfg,
216+ workspaceDir,
217+ agentId,
218+ store,
219+ } ) ;
220+ state . set ( provider , value ) ;
221+ }
222+ states . set ( agentId , {
223+ agentId,
224+ configFingerprint,
225+ providers : state ,
175226 } ) ;
176- state . set ( provider , value ) ;
177227 }
178228 if ( ownGeneration !== currentProviderAuthStateGeneration ) {
179229 // A newer warm or clear ran while we were building; skip publication so
180230 // the newer answer wins.
181231 return ;
182232 }
183- currentProviderAuthState = {
184- configFingerprint : resolveProviderAuthConfigFingerprint ( cfg ) ?? "" ,
185- workspaceDir,
186- preparedAtMs : Date . now ( ) ,
187- providers : state ,
188- } ;
233+ currentProviderAuthStates = states ;
189234}
0 commit comments