Skip to content

Commit 95343af

Browse files
committed
Remove ttl on auth config. Prewarm prepared config for each agent. Key by agent ID instead of agent dir
1 parent 1008b82 commit 95343af

5 files changed

Lines changed: 102 additions & 75 deletions

File tree

src/agents/model-catalog-visibility.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ export function resolveVisibleModelCatalog(params: {
3232
defaultProvider: string;
3333
defaultModel?: string;
3434
agentId?: string;
35-
agentDir?: string;
3635
workspaceDir?: string;
3736
env?: NodeJS.ProcessEnv;
3837
view?: ModelCatalogVisibilityView;
@@ -52,7 +51,7 @@ export function resolveVisibleModelCatalog(params: {
5251
createProviderAuthChecker({
5352
cfg: params.cfg,
5453
workspaceDir: params.workspaceDir,
55-
agentDir: params.agentDir,
54+
agentId: params.agentId,
5655
env: params.env,
5756
allowPluginSyntheticAuth: params.runtimeAuthDiscovery,
5857
discoverExternalCliAuth: params.runtimeAuthDiscovery,

src/agents/model-provider-auth.test.ts

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,18 @@ vi.mock("./workspace.js", () => ({
4040
resolveDefaultAgentWorkspaceDir: () => "/warm/default-workspace",
4141
}));
4242

43+
vi.mock("./agent-scope-config.js", () => ({
44+
listAgentIds: () => ["default"],
45+
resolveAgentDir: () => "/warm/default-agent",
46+
resolveAgentWorkspaceDir: () => "/warm/default-workspace",
47+
resolveDefaultAgentId: () => "default",
48+
}));
49+
4350
const { clearCurrentProviderAuthState, hasAuthForModelProvider, warmCurrentProviderAuthState } =
4451
await import("./model-provider-auth.js");
4552

4653
describe("prepared provider auth state", () => {
4754
afterEach(() => {
48-
vi.useRealTimers();
4955
clearCurrentProviderAuthState();
5056
vi.clearAllMocks();
5157
});
@@ -122,26 +128,6 @@ describe("prepared provider auth state", () => {
122128
expect(modelAuthMocks.hasRuntimeAvailableProviderAuth).toHaveBeenCalledTimes(1);
123129
});
124130

125-
it("hasAuthForModelProvider falls through after the prepared auth state TTL", async () => {
126-
vi.useFakeTimers();
127-
vi.setSystemTime(0);
128-
const cfg = {} as OpenClawConfig;
129-
modelCatalogMocks.loadModelCatalog.mockResolvedValue([
130-
{ id: "gpt", name: "gpt", provider: "openai" },
131-
]);
132-
modelAuthMocks.hasRuntimeAvailableProviderAuth.mockReturnValue(false);
133-
await warmCurrentProviderAuthState(cfg);
134-
expect(modelAuthMocks.hasRuntimeAvailableProviderAuth).toHaveBeenCalledTimes(1);
135-
136-
modelAuthMocks.hasRuntimeAvailableProviderAuth.mockReturnValue(true);
137-
expect(hasAuthForModelProvider({ provider: "openai", cfg })).toBe(false);
138-
expect(modelAuthMocks.hasRuntimeAvailableProviderAuth).toHaveBeenCalledTimes(1);
139-
140-
vi.setSystemTime(10_001);
141-
expect(hasAuthForModelProvider({ provider: "openai", cfg })).toBe(true);
142-
expect(modelAuthMocks.hasRuntimeAvailableProviderAuth).toHaveBeenCalledTimes(2);
143-
});
144-
145131
it("hasAuthForModelProvider falls through to compute when the caller passes a non-default workspaceDir", async () => {
146132
const cfg = {} as OpenClawConfig;
147133
modelCatalogMocks.loadModelCatalog.mockResolvedValue([

src/agents/model-provider-auth.ts

Lines changed: 93 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { hashRuntimeConfigValue } from "../config/runtime-snapshot.js";
22
import type { OpenClawConfig } from "../config/types.openclaw.js";
3+
import {
4+
listAgentIds,
5+
resolveAgentDir,
6+
resolveAgentWorkspaceDir,
7+
resolveDefaultAgentId,
8+
} from "./agent-scope-config.js";
39
import {
410
externalCliDiscoveryForProviderAuth,
511
externalCliDiscoveryForProviders,
@@ -20,24 +26,43 @@ import { resolveDefaultAgentWorkspaceDir } from "./workspace.js";
2026
// discovery and external-CLI probing on the hot path.
2127

2228
type 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;
3138
const 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.
3441
let currentProviderAuthStateGeneration = 0;
3542

3643
export 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+
4166
function 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: {
119154
export 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
}

src/auto-reply/reply/commands-models.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ export async function buildModelsProviderData(
254254
options.workspaceDir ??
255255
(agentId ? resolveAgentWorkspaceDir(cfg, agentId) : undefined) ??
256256
resolveDefaultAgentWorkspaceDir(),
257-
agentDir: agentId ? resolveAgentDir(cfg, agentId) : undefined,
257+
agentId,
258258
});
259259

260260
for (const entry of catalog) {

src/flows/model-picker.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -730,7 +730,6 @@ export async function promptDefaultModel(
730730
catalog,
731731
defaultProvider: DEFAULT_PROVIDER,
732732
defaultModel: resolved.model,
733-
agentDir: params.agentDir,
734733
workspaceDir: params.workspaceDir,
735734
env: params.env,
736735
});
@@ -771,7 +770,6 @@ export async function promptDefaultModel(
771770
const hasAuth = createProviderAuthChecker({
772771
cfg,
773772
workspaceDir: params.workspaceDir,
774-
agentDir: params.agentDir,
775773
env: params.env,
776774
});
777775
const literalPrefixProviders = await resolveCachedLiteralPrefixProviders();
@@ -937,7 +935,6 @@ export async function promptModelAllowlist(params: {
937935
const hasAuth = createProviderAuthChecker({
938936
cfg,
939937
workspaceDir: params.workspaceDir,
940-
agentDir: params.agentDir,
941938
env: params.env,
942939
});
943940
const matchesPreferredProvider = preferredProvider

0 commit comments

Comments
 (0)