mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-28 05:16:17 +00:00
Remove ttl on auth config. Prewarm prepared config for each agent. Key by agent ID instead of agent dir
This commit is contained in:
@@ -32,7 +32,6 @@ export function resolveVisibleModelCatalog(params: {
|
||||
defaultProvider: string;
|
||||
defaultModel?: string;
|
||||
agentId?: string;
|
||||
agentDir?: string;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
view?: ModelCatalogVisibilityView;
|
||||
@@ -52,7 +51,7 @@ export function resolveVisibleModelCatalog(params: {
|
||||
createProviderAuthChecker({
|
||||
cfg: params.cfg,
|
||||
workspaceDir: params.workspaceDir,
|
||||
agentDir: params.agentDir,
|
||||
agentId: params.agentId,
|
||||
env: params.env,
|
||||
allowPluginSyntheticAuth: params.runtimeAuthDiscovery,
|
||||
discoverExternalCliAuth: params.runtimeAuthDiscovery,
|
||||
|
||||
@@ -40,12 +40,18 @@ vi.mock("./workspace.js", () => ({
|
||||
resolveDefaultAgentWorkspaceDir: () => "/warm/default-workspace",
|
||||
}));
|
||||
|
||||
vi.mock("./agent-scope-config.js", () => ({
|
||||
listAgentIds: () => ["default"],
|
||||
resolveAgentDir: () => "/warm/default-agent",
|
||||
resolveAgentWorkspaceDir: () => "/warm/default-workspace",
|
||||
resolveDefaultAgentId: () => "default",
|
||||
}));
|
||||
|
||||
const { clearCurrentProviderAuthState, hasAuthForModelProvider, warmCurrentProviderAuthState } =
|
||||
await import("./model-provider-auth.js");
|
||||
|
||||
describe("prepared provider auth state", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
clearCurrentProviderAuthState();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
@@ -122,26 +128,6 @@ describe("prepared provider auth state", () => {
|
||||
expect(modelAuthMocks.hasRuntimeAvailableProviderAuth).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("hasAuthForModelProvider falls through after the prepared auth state TTL", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(0);
|
||||
const cfg = {} as OpenClawConfig;
|
||||
modelCatalogMocks.loadModelCatalog.mockResolvedValue([
|
||||
{ id: "gpt", name: "gpt", provider: "openai" },
|
||||
]);
|
||||
modelAuthMocks.hasRuntimeAvailableProviderAuth.mockReturnValue(false);
|
||||
await warmCurrentProviderAuthState(cfg);
|
||||
expect(modelAuthMocks.hasRuntimeAvailableProviderAuth).toHaveBeenCalledTimes(1);
|
||||
|
||||
modelAuthMocks.hasRuntimeAvailableProviderAuth.mockReturnValue(true);
|
||||
expect(hasAuthForModelProvider({ provider: "openai", cfg })).toBe(false);
|
||||
expect(modelAuthMocks.hasRuntimeAvailableProviderAuth).toHaveBeenCalledTimes(1);
|
||||
|
||||
vi.setSystemTime(10_001);
|
||||
expect(hasAuthForModelProvider({ provider: "openai", cfg })).toBe(true);
|
||||
expect(modelAuthMocks.hasRuntimeAvailableProviderAuth).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("hasAuthForModelProvider falls through to compute when the caller passes a non-default workspaceDir", async () => {
|
||||
const cfg = {} as OpenClawConfig;
|
||||
modelCatalogMocks.loadModelCatalog.mockResolvedValue([
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { hashRuntimeConfigValue } from "../config/runtime-snapshot.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import {
|
||||
listAgentIds,
|
||||
resolveAgentDir,
|
||||
resolveAgentWorkspaceDir,
|
||||
resolveDefaultAgentId,
|
||||
} from "./agent-scope-config.js";
|
||||
import {
|
||||
externalCliDiscoveryForProviderAuth,
|
||||
externalCliDiscoveryForProviders,
|
||||
@@ -20,24 +26,43 @@ import { resolveDefaultAgentWorkspaceDir } from "./workspace.js";
|
||||
// discovery and external-CLI probing on the hot path.
|
||||
|
||||
type PreparedProviderAuthState = {
|
||||
agentId: string;
|
||||
configFingerprint: string;
|
||||
workspaceDir: string;
|
||||
preparedAtMs: number;
|
||||
providers: ReadonlyMap<string, boolean>;
|
||||
};
|
||||
|
||||
const PREPARED_PROVIDER_AUTH_STATE_TTL_MS = 10_000;
|
||||
let currentProviderAuthState: PreparedProviderAuthState | null = null;
|
||||
// One entry per configured agent, keyed by agentId. Populated by
|
||||
// warmCurrentProviderAuthState at gateway startup / on reload; consulted by
|
||||
// hasAuthForModelProvider on every model-listing call.
|
||||
let currentProviderAuthStates: ReadonlyMap<string, PreparedProviderAuthState> | null = null;
|
||||
const configFingerprintCache = new WeakMap<OpenClawConfig, string>();
|
||||
// Generation counter guards against an in-flight warm publishing stale
|
||||
// state after a subsequent warm or clear has invalidated it.
|
||||
let currentProviderAuthStateGeneration = 0;
|
||||
|
||||
export function clearCurrentProviderAuthState(): void {
|
||||
currentProviderAuthState = null;
|
||||
currentProviderAuthStates = null;
|
||||
currentProviderAuthStateGeneration += 1;
|
||||
}
|
||||
|
||||
function resolvePreparedStateForCaller(params: {
|
||||
states: ReadonlyMap<string, PreparedProviderAuthState> | null;
|
||||
cfg: OpenClawConfig | undefined;
|
||||
callerAgentId: string | undefined;
|
||||
}): PreparedProviderAuthState | null {
|
||||
if (!params.states) {
|
||||
return null;
|
||||
}
|
||||
if (params.callerAgentId !== undefined) {
|
||||
return params.states.get(params.callerAgentId) ?? null;
|
||||
}
|
||||
// Caller didn't pass agentId: treat as a query against the default agent.
|
||||
if (!params.cfg) {
|
||||
return null;
|
||||
}
|
||||
return params.states.get(resolveDefaultAgentId(params.cfg)) ?? null;
|
||||
}
|
||||
|
||||
function resolveProviderAuthConfigFingerprint(cfg: OpenClawConfig | undefined): string | null {
|
||||
if (!cfg) {
|
||||
return null;
|
||||
@@ -55,33 +80,41 @@ export function hasAuthForModelProvider(params: {
|
||||
provider: string;
|
||||
cfg?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
agentDir?: string;
|
||||
agentId?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
store?: AuthProfileStore;
|
||||
allowPluginSyntheticAuth?: boolean;
|
||||
discoverExternalCliAuth?: boolean;
|
||||
}): boolean {
|
||||
const provider = normalizeProviderId(params.provider);
|
||||
// The prepared map is built by warmCurrentProviderAuthState with broad
|
||||
// auth discovery (external CLI + plugin synthetic auth enabled) and the
|
||||
// default-agent workspace dir. Only consult it when the caller's full
|
||||
// auth context matches; otherwise fall through to compute so callers
|
||||
// that narrow the scope — e.g. gateway `models.list` with
|
||||
// `runtimeAuthDiscovery: false`, or per-agent picker calls that pass a
|
||||
// non-default workspaceDir — get the answer they asked for.
|
||||
const preparedState = currentProviderAuthState;
|
||||
// The prepared map is built by warmCurrentProviderAuthState — one entry per
|
||||
// configured agent, keyed by agentId. Only consult it when the caller's
|
||||
// full auth context matches the warmed scope; otherwise fall through to
|
||||
// compute so callers that narrow the scope — e.g. gateway `models.list`
|
||||
// with `runtimeAuthDiscovery: false`, or callers with a non-warmed
|
||||
// workspaceDir — get the answer they asked for.
|
||||
const preparedStates = currentProviderAuthStates;
|
||||
const workspaceDir = params.workspaceDir ?? resolveDefaultAgentWorkspaceDir();
|
||||
const configFingerprint = resolveProviderAuthConfigFingerprint(params.cfg);
|
||||
const preparedStateFresh =
|
||||
preparedState !== null &&
|
||||
Date.now() - preparedState.preparedAtMs <= PREPARED_PROVIDER_AUTH_STATE_TTL_MS;
|
||||
const preparedState = resolvePreparedStateForCaller({
|
||||
states: preparedStates,
|
||||
cfg: params.cfg,
|
||||
callerAgentId: params.agentId,
|
||||
});
|
||||
// workspaceDir is a pure function of (cfg, agentId), so we recompute the
|
||||
// warmer's expected value at read time rather than storing it. Caller can
|
||||
// still override workspaceDir explicitly — that forces a mismatch and
|
||||
// falls through to the compute path.
|
||||
const expectedWorkspaceDir =
|
||||
preparedState !== null && params.cfg
|
||||
? resolveAgentWorkspaceDir(params.cfg, preparedState.agentId)
|
||||
: null;
|
||||
const matchesWarmedScope =
|
||||
preparedStateFresh &&
|
||||
preparedState !== null &&
|
||||
configFingerprint === preparedState.configFingerprint &&
|
||||
workspaceDir === preparedState.workspaceDir &&
|
||||
workspaceDir === expectedWorkspaceDir &&
|
||||
params.discoverExternalCliAuth !== false &&
|
||||
params.allowPluginSyntheticAuth !== false &&
|
||||
params.agentDir === undefined &&
|
||||
params.env === undefined &&
|
||||
params.store === undefined;
|
||||
if (matchesWarmedScope) {
|
||||
@@ -101,13 +134,15 @@ export function hasAuthForModelProvider(params: {
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const slowPathAgentDir =
|
||||
params.agentId && params.cfg ? resolveAgentDir(params.cfg, params.agentId) : undefined;
|
||||
const store =
|
||||
params.store ??
|
||||
(params.discoverExternalCliAuth === false
|
||||
? ensureAuthProfileStoreWithoutExternalProfiles(params.agentDir, {
|
||||
? ensureAuthProfileStoreWithoutExternalProfiles(slowPathAgentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
})
|
||||
: ensureAuthProfileStore(params.agentDir, {
|
||||
: ensureAuthProfileStore(slowPathAgentDir, {
|
||||
externalCli: externalCliDiscoveryForProviderAuth({ cfg: params.cfg, provider }),
|
||||
}));
|
||||
if (listProfilesForProvider(store, provider).length > 0) {
|
||||
@@ -119,7 +154,7 @@ export function hasAuthForModelProvider(params: {
|
||||
export function createProviderAuthChecker(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
agentDir?: string;
|
||||
agentId?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
allowPluginSyntheticAuth?: boolean;
|
||||
discoverExternalCliAuth?: boolean;
|
||||
@@ -135,7 +170,7 @@ export function createProviderAuthChecker(params: {
|
||||
provider: key,
|
||||
cfg: params.cfg,
|
||||
workspaceDir: params.workspaceDir,
|
||||
agentDir: params.agentDir,
|
||||
agentId: params.agentId,
|
||||
env: params.env,
|
||||
allowPluginSyntheticAuth: params.allowPluginSyntheticAuth,
|
||||
discoverExternalCliAuth: params.discoverExternalCliAuth,
|
||||
@@ -155,35 +190,45 @@ export async function warmCurrentProviderAuthState(cfg: OpenClawConfig): Promise
|
||||
for (const entry of catalog) {
|
||||
providers.add(normalizeProviderId(entry.provider));
|
||||
}
|
||||
const workspaceDir = resolveDefaultAgentWorkspaceDir();
|
||||
// One AuthProfileStore scoped to every candidate provider; without this the
|
||||
// per-provider externalCli discovery rebuilds the store ~N times.
|
||||
const store = ensureAuthProfileStore(undefined, {
|
||||
config: cfg,
|
||||
externalCli: externalCliDiscoveryForProviders({
|
||||
cfg,
|
||||
providers: [...providers],
|
||||
}),
|
||||
});
|
||||
const state = new Map<string, boolean>();
|
||||
for (const provider of providers) {
|
||||
const value = hasAuthForModelProvider({
|
||||
provider,
|
||||
cfg,
|
||||
workspaceDir,
|
||||
store,
|
||||
const providerList = [...providers];
|
||||
const configFingerprint = resolveProviderAuthConfigFingerprint(cfg) ?? "";
|
||||
const states = new Map<string, PreparedProviderAuthState>();
|
||||
// Warm one entry per configured agent so callers hit the prepared map for
|
||||
// any agentId. The catalog above is shared across agents; the per-agent
|
||||
// work is the auth-discovery sweep against that agent's store.
|
||||
for (const agentId of listAgentIds(cfg)) {
|
||||
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
||||
const agentDir = resolveAgentDir(cfg, agentId);
|
||||
// One AuthProfileStore scoped to every candidate provider; without this
|
||||
// the per-provider externalCli discovery rebuilds the store ~N times.
|
||||
const store = ensureAuthProfileStore(agentDir, {
|
||||
config: cfg,
|
||||
externalCli: externalCliDiscoveryForProviders({
|
||||
cfg,
|
||||
providers: providerList,
|
||||
}),
|
||||
});
|
||||
const state = new Map<string, boolean>();
|
||||
for (const provider of providers) {
|
||||
const value = hasAuthForModelProvider({
|
||||
provider,
|
||||
cfg,
|
||||
workspaceDir,
|
||||
agentId,
|
||||
store,
|
||||
});
|
||||
state.set(provider, value);
|
||||
}
|
||||
states.set(agentId, {
|
||||
agentId,
|
||||
configFingerprint,
|
||||
providers: state,
|
||||
});
|
||||
state.set(provider, value);
|
||||
}
|
||||
if (ownGeneration !== currentProviderAuthStateGeneration) {
|
||||
// A newer warm or clear ran while we were building; skip publication so
|
||||
// the newer answer wins.
|
||||
return;
|
||||
}
|
||||
currentProviderAuthState = {
|
||||
configFingerprint: resolveProviderAuthConfigFingerprint(cfg) ?? "",
|
||||
workspaceDir,
|
||||
preparedAtMs: Date.now(),
|
||||
providers: state,
|
||||
};
|
||||
currentProviderAuthStates = states;
|
||||
}
|
||||
|
||||
@@ -254,7 +254,7 @@ export async function buildModelsProviderData(
|
||||
options.workspaceDir ??
|
||||
(agentId ? resolveAgentWorkspaceDir(cfg, agentId) : undefined) ??
|
||||
resolveDefaultAgentWorkspaceDir(),
|
||||
agentDir: agentId ? resolveAgentDir(cfg, agentId) : undefined,
|
||||
agentId,
|
||||
});
|
||||
|
||||
for (const entry of catalog) {
|
||||
|
||||
@@ -730,7 +730,6 @@ export async function promptDefaultModel(
|
||||
catalog,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
defaultModel: resolved.model,
|
||||
agentDir: params.agentDir,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
@@ -771,7 +770,6 @@ export async function promptDefaultModel(
|
||||
const hasAuth = createProviderAuthChecker({
|
||||
cfg,
|
||||
workspaceDir: params.workspaceDir,
|
||||
agentDir: params.agentDir,
|
||||
env: params.env,
|
||||
});
|
||||
const literalPrefixProviders = await resolveCachedLiteralPrefixProviders();
|
||||
@@ -937,7 +935,6 @@ export async function promptModelAllowlist(params: {
|
||||
const hasAuth = createProviderAuthChecker({
|
||||
cfg,
|
||||
workspaceDir: params.workspaceDir,
|
||||
agentDir: params.agentDir,
|
||||
env: params.env,
|
||||
});
|
||||
const matchesPreferredProvider = preferredProvider
|
||||
|
||||
Reference in New Issue
Block a user