fix(agents): scope external CLI auth discovery

This commit is contained in:
Peter Steinberger
2026-04-29 07:52:13 +01:00
parent 3367cfaa14
commit 13757465ba
12 changed files with 431 additions and 15 deletions

View File

@@ -47,6 +47,7 @@ Docs: https://docs.openclaw.ai
- Channels/WhatsApp: flag recently reconnected linked accounts in channel status even when the socket is currently healthy, so flapping WhatsApp Web sessions no longer look clean after a brief reconnect. Refs #73602. Thanks @Vksh07.
- Gateway: expose `gateway.handshakeTimeoutMs` in config, schema, and docs while preserving `OPENCLAW_HANDSHAKE_TIMEOUT_MS` precedence, so loaded or low-powered hosts can tune local WebSocket pre-auth handshakes without patching dist files. Supersedes #51282; refs #73592 and #73652. Thanks @henry-the-frog.
- Gateway/TUI/status: align configured and env-based WebSocket handshake budgets across local clients, probes, and fallback RPCs while preserving explicit status timeouts and paired-device auth fallback, so slow local gateways are not marked unreachable by a shorter client watchdog. Refs #73524, #73535, #73592, and #73602. Thanks @harshcatsystems-collab, @DJBlackhawk, and @Vksh07.
- Agents/auth: scope external CLI credential discovery to configured providers during model auth status and startup prewarm, so opencode-only and other single-provider gateways do not block on unrelated Claude CLI Keychain probes. Fixes #73908. Thanks @Ailuras.
- Agents/model selection: resolve slash-form aliases before provider/model parsing and keep alias-resolved primary models subject to transient provider cooldowns, so cron and persisted sessions do not retry cooled-down raw aliases. Fixes #73573 and #73657. Thanks @akai-shuuichi and @hashslingers.
- Agents/Claude CLI: reuse already-cached macOS Keychain credentials for no-prompt Claude credential reads, so doctor/runtime checks do not miss fresh interactive Claude auth. Fixes #73682. Thanks @RyanSandoval.
- Agents/Claude CLI doctor: scope workspace and project-dir checks to agents that actually use the Claude CLI runtime, so non-default Claude agents no longer make the default agent look Claude-backed. Fixes #73903. Thanks @bobfreeman1989.

View File

@@ -48,6 +48,9 @@ To reduce that, OpenClaw treats `auth-profiles.json` as a **token sink**:
`openai-codex:default` profile, but once OpenClaw has a local OAuth profile,
the local refresh token is canonical; other integrations can remain
externally managed and re-read their CLI auth store
- status and startup paths that already know the configured provider set scope
external CLI discovery to that set, so an unrelated CLI login store is not
probed for a single-provider setup
## Storage (where tokens live)

View File

@@ -0,0 +1,85 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolveExternalCliAuthScopeFromConfig } from "./auth-profiles/external-cli-scope.js";
describe("external CLI auth scope", () => {
it("returns undefined when config has no provider signal", () => {
expect(resolveExternalCliAuthScopeFromConfig({})).toBeUndefined();
});
it("scopes opencode-only config without adding unrelated CLI providers", () => {
const scope = resolveExternalCliAuthScopeFromConfig({
auth: {
profiles: {
"opencode-go:default": { provider: "opencode-go", mode: "api_key" },
},
},
agents: {
defaults: {
model: { primary: "opencode-go/kimi-k2.6" },
},
},
models: {
providers: {
"opencode-go": {
baseUrl: "https://example.test/v1",
auth: "api-key",
models: [],
},
},
},
});
expect(scope?.providerIds).toContain("opencode-go");
expect(scope?.profileIds).toEqual(["opencode-go:default"]);
expect(scope?.providerIds).not.toContain("claude-cli");
expect(scope?.providerIds).not.toContain("openai-codex");
expect(scope?.providerIds).not.toContain("minimax-portal");
});
it("collects model, auth order, media model, and runtime signals", () => {
const cfg = {
auth: {
order: {
"openai-codex": ["openai-codex:default"],
},
},
agents: {
defaults: {
model: {
primary: "anthropic/claude-opus-4-7",
fallbacks: ["openai/gpt-5.5"],
},
imageGenerationModel: "minimax-portal/image-01",
cliBackends: {
"claude-cli": { command: "claude" },
},
},
list: [
{
id: "worker",
model: "opencode-go/kimi-k2.6",
agentRuntime: { id: "codex" },
subagents: { model: { primary: "z.ai/glm-4.7" } },
},
],
},
} satisfies OpenClawConfig;
const scope = resolveExternalCliAuthScopeFromConfig(cfg);
expect(scope?.providerIds).toEqual(
expect.arrayContaining([
"anthropic",
"openai",
"openai-codex",
"minimax-portal",
"claude-cli",
"codex",
"opencode-go",
"z.ai",
"zai",
]),
);
});
});

View File

@@ -3,9 +3,11 @@ import type { AuthProfileStore, OAuthCredential } from "./auth-profiles/types.js
import type { ClaudeCliCredential } from "./cli-credentials.js";
const mocks = vi.hoisted(() => ({
readClaudeCliCredentialsCached: vi.fn<() => ClaudeCliCredential | null>(() => null),
readCodexCliCredentialsCached: vi.fn<() => OAuthCredential | null>(() => null),
readMiniMaxCliCredentialsCached: vi.fn<() => OAuthCredential | null>(() => null),
readClaudeCliCredentialsCached: vi.fn<(options?: unknown) => ClaudeCliCredential | null>(
() => null,
),
readCodexCliCredentialsCached: vi.fn<(options?: unknown) => OAuthCredential | null>(() => null),
readMiniMaxCliCredentialsCached: vi.fn<(options?: unknown) => OAuthCredential | null>(() => null),
}));
let readManagedExternalCliCredential: typeof import("./auth-profiles/external-cli-sync.js").readManagedExternalCliCredential;
@@ -331,6 +333,47 @@ describe("external cli oauth resolution", () => {
]);
});
it("skips external cli readers outside the scoped provider set", () => {
const profiles = resolveExternalCliAuthProfiles(makeStore(), {
providerIds: ["opencode-go"],
});
expect(profiles).toEqual([]);
expect(mocks.readCodexCliCredentialsCached).not.toHaveBeenCalled();
expect(mocks.readClaudeCliCredentialsCached).not.toHaveBeenCalled();
expect(mocks.readMiniMaxCliCredentialsCached).not.toHaveBeenCalled();
});
it("passes non-prompting keychain policy to scoped Claude CLI credential reads", () => {
mocks.readClaudeCliCredentialsCached.mockReturnValue({
type: "oauth",
provider: "anthropic",
access: "claude-cli-access",
refresh: "claude-cli-refresh",
expires: Date.now() + 5 * 24 * 60 * 60_000,
});
const profiles = resolveExternalCliAuthProfiles(makeStore(), {
providerIds: ["claude-cli"],
allowKeychainPrompt: false,
});
expect(profiles).toEqual([
{
profileId: CLAUDE_CLI_PROFILE_ID,
credential: expect.objectContaining({
type: "oauth",
provider: "claude-cli",
}),
},
]);
expect(mocks.readClaudeCliCredentialsCached).toHaveBeenCalledWith(
expect.objectContaining({ allowKeychainPrompt: false }),
);
expect(mocks.readCodexCliCredentialsCached).not.toHaveBeenCalled();
expect(mocks.readMiniMaxCliCredentialsCached).not.toHaveBeenCalled();
});
it("ignores Claude CLI token credentials", () => {
mocks.readClaudeCliCredentialsCached.mockReturnValue({
type: "token",

View File

@@ -12,7 +12,9 @@ import type { OAuthCredential } from "./auth-profiles/types.js";
type RuntimeOnlyOverlay = { profileId: string; credential: OAuthCredential };
const mocks = vi.hoisted(() => ({
resolveExternalCliAuthProfiles: vi.fn<() => RuntimeOnlyOverlay[]>(() => []),
resolveExternalCliAuthProfiles: vi.fn<
(store?: unknown, options?: unknown) => RuntimeOnlyOverlay[]
>(() => []),
}));
vi.mock("./auth-profiles/external-cli-sync.js", () => ({

View File

@@ -10,6 +10,11 @@ import type { AuthProfileStore, OAuthCredential } from "./types.js";
type ExternalAuthProfileMap = Map<string, ProviderExternalAuthProfile>;
type ResolveExternalAuthProfiles = typeof resolveExternalAuthProfilesWithPlugins;
type ExternalCliOverlayOptions = {
allowKeychainPrompt?: boolean;
externalCliProviderIds?: Iterable<string>;
externalCliProfileIds?: Iterable<string>;
};
let resolveExternalAuthProfilesForRuntime: ResolveExternalAuthProfiles | undefined;
@@ -38,6 +43,7 @@ function resolveExternalAuthProfileMap(params: {
store: AuthProfileStore;
agentDir?: string;
env?: NodeJS.ProcessEnv;
externalCli?: ExternalCliOverlayOptions;
}): ExternalAuthProfileMap {
const env = params.env ?? process.env;
const resolveProfiles =
@@ -54,7 +60,12 @@ function resolveExternalAuthProfileMap(params: {
});
const resolved: ExternalAuthProfileMap = new Map();
const cliProfiles = externalCliSync.resolveExternalCliAuthProfiles?.(params.store) ?? [];
const cliProfiles =
externalCliSync.resolveExternalCliAuthProfiles?.(params.store, {
allowKeychainPrompt: params.externalCli?.allowKeychainPrompt,
providerIds: params.externalCli?.externalCliProviderIds,
profileIds: params.externalCli?.externalCliProfileIds,
}) ?? [];
for (const profile of cliProfiles) {
resolved.set(profile.profileId, {
profileId: profile.profileId,
@@ -76,24 +87,27 @@ function listRuntimeExternalAuthProfiles(params: {
store: AuthProfileStore;
agentDir?: string;
env?: NodeJS.ProcessEnv;
externalCli?: ExternalCliOverlayOptions;
}): RuntimeExternalOAuthProfile[] {
return Array.from(
resolveExternalAuthProfileMap({
store: params.store,
agentDir: params.agentDir,
env: params.env,
externalCli: params.externalCli,
}).values(),
);
}
export function overlayExternalAuthProfiles(
store: AuthProfileStore,
params?: { agentDir?: string; env?: NodeJS.ProcessEnv },
params?: { agentDir?: string; env?: NodeJS.ProcessEnv } & ExternalCliOverlayOptions,
): AuthProfileStore {
const profiles = listRuntimeExternalAuthProfiles({
store,
agentDir: params?.agentDir,
env: params?.env,
externalCli: params,
});
return overlayRuntimeExternalOAuthProfiles(store, profiles);
}
@@ -104,11 +118,17 @@ export function shouldPersistExternalAuthProfile(params: {
credential: OAuthCredential;
agentDir?: string;
env?: NodeJS.ProcessEnv;
externalCliProviderIds?: Iterable<string>;
externalCliProfileIds?: Iterable<string>;
}): boolean {
const profiles = listRuntimeExternalAuthProfiles({
store: params.store,
agentDir: params.agentDir,
env: params.env,
externalCli: {
externalCliProviderIds: params.externalCliProviderIds,
externalCliProfileIds: params.externalCliProfileIds,
},
});
return shouldPersistRuntimeExternalOAuthProfile({
profileId: params.profileId,

View File

@@ -0,0 +1,110 @@
import {
resolveAgentModelFallbackValues,
resolveAgentModelPrimaryValue,
} from "../../config/model-input.js";
import type { AgentModelConfig } from "../../config/types.agents-shared.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { normalizeProviderId } from "../provider-id.js";
export type ExternalCliAuthScope = {
providerIds: string[];
profileIds: string[];
};
function addProviderScopeId(out: Set<string>, value: string | undefined): void {
const raw = value?.trim();
if (!raw) {
return;
}
out.add(raw);
const normalized = normalizeProviderId(raw);
if (normalized) {
out.add(normalized);
}
}
function addProviderScopeFromModelRef(out: Set<string>, value: string | undefined): void {
const raw = value?.trim();
if (!raw) {
return;
}
const slash = raw.indexOf("/");
if (slash <= 0) {
return;
}
addProviderScopeId(out, raw.slice(0, slash));
}
function addProviderScopeFromModelConfig(out: Set<string>, model: AgentModelConfig | undefined) {
addProviderScopeFromModelRef(out, resolveAgentModelPrimaryValue(model));
for (const fallback of resolveAgentModelFallbackValues(model)) {
addProviderScopeFromModelRef(out, fallback);
}
}
function addExternalCliRuntimeScope(out: Set<string>, value: string | undefined): void {
const normalized = normalizeProviderId(value?.trim() ?? "");
if (
normalized === "claude-cli" ||
normalized === "codex" ||
normalized === "codex-cli" ||
normalized === "openai-codex" ||
normalized === "minimax" ||
normalized === "minimax-cli" ||
normalized === "minimax-portal"
) {
addProviderScopeId(out, normalized);
}
}
export function resolveExternalCliAuthScopeFromConfig(
cfg: OpenClawConfig,
): ExternalCliAuthScope | undefined {
const providerIds = new Set<string>();
const profileIds = new Set<string>();
for (const id of Object.keys(cfg.models?.providers ?? {})) {
addProviderScopeId(providerIds, id);
}
for (const [profileId, profile] of Object.entries(cfg.auth?.profiles ?? {})) {
const normalizedProfileId = profileId.trim();
if (normalizedProfileId) {
profileIds.add(normalizedProfileId);
}
addProviderScopeId(providerIds, profile?.provider);
}
for (const provider of Object.keys(cfg.auth?.order ?? {})) {
addProviderScopeId(providerIds, provider);
}
const defaults = cfg.agents?.defaults;
addProviderScopeFromModelConfig(providerIds, defaults?.model);
addProviderScopeFromModelConfig(providerIds, defaults?.imageModel);
addProviderScopeFromModelConfig(providerIds, defaults?.imageGenerationModel);
addProviderScopeFromModelConfig(providerIds, defaults?.videoGenerationModel);
addProviderScopeFromModelConfig(providerIds, defaults?.musicGenerationModel);
addProviderScopeFromModelConfig(providerIds, defaults?.pdfModel);
for (const modelRef of Object.keys(defaults?.models ?? {})) {
addProviderScopeFromModelRef(providerIds, modelRef);
}
addExternalCliRuntimeScope(providerIds, defaults?.agentRuntime?.id);
addExternalCliRuntimeScope(providerIds, defaults?.embeddedHarness?.runtime);
for (const backendId of Object.keys(defaults?.cliBackends ?? {})) {
addExternalCliRuntimeScope(providerIds, backendId);
}
for (const agent of cfg.agents?.list ?? []) {
addProviderScopeFromModelConfig(providerIds, agent.model);
addProviderScopeFromModelConfig(providerIds, agent.subagents?.model);
addExternalCliRuntimeScope(providerIds, agent.agentRuntime?.id);
addExternalCliRuntimeScope(providerIds, agent.embeddedHarness?.runtime);
}
if (providerIds.size === 0 && profileIds.size === 0) {
return undefined;
}
return {
providerIds: [...providerIds].toSorted((left, right) => left.localeCompare(right)),
profileIds: [...profileIds].toSorted((left, right) => left.localeCompare(right)),
};
}

View File

@@ -3,6 +3,7 @@ import {
readCodexCliCredentialsCached,
readMiniMaxCliCredentialsCached,
} from "../cli-credentials.js";
import { normalizeProviderId } from "../provider-id.js";
import {
CLAUDE_CLI_PROFILE_ID,
EXTERNAL_CLI_SYNC_TTL_MS,
@@ -34,10 +35,19 @@ export type ExternalCliResolvedProfile = {
credential: OAuthCredential;
};
export type ExternalCliAuthProfileOptions = {
allowKeychainPrompt?: boolean;
providerIds?: Iterable<string>;
profileIds?: Iterable<string>;
};
type ExternalCliSyncProvider = {
profileId: string;
provider: string;
readCredentials: () => OAuthCredential | null;
aliases?: readonly string[];
readCredentials: (
options?: Pick<ExternalCliAuthProfileOptions, "allowKeychainPrompt">,
) => OAuthCredential | null;
// bootstrapOnly providers adopt the external CLI credential only to
// seed an empty slot; once a local OAuth credential exists for the
// profile, the local refresh token is treated as canonical and the
@@ -90,14 +100,18 @@ const EXTERNAL_CLI_SYNC_PROVIDERS: ExternalCliSyncProvider[] = [
{
profileId: OPENAI_CODEX_DEFAULT_PROFILE_ID,
provider: "openai-codex",
aliases: ["codex", "codex-cli"],
readCredentials: () => readCodexCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS }),
bootstrapOnly: true,
},
{
profileId: CLAUDE_CLI_PROFILE_ID,
provider: "claude-cli",
readCredentials: () => {
const credential = readClaudeCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS });
readCredentials: (options) => {
const credential = readClaudeCliCredentialsCached({
ttlMs: EXTERNAL_CLI_SYNC_TTL_MS,
allowKeychainPrompt: options?.allowKeychainPrompt,
});
if (credential?.type !== "oauth") {
return null;
}
@@ -107,6 +121,7 @@ const EXTERNAL_CLI_SYNC_PROVIDERS: ExternalCliSyncProvider[] = [
{
profileId: MINIMAX_CLI_PROFILE_ID,
provider: "minimax-portal",
aliases: ["minimax", "minimax-cli"],
readCredentials: () => readMiniMaxCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS }),
},
];
@@ -147,13 +162,75 @@ export function readExternalCliBootstrapCredential(params: {
export const readManagedExternalCliCredential = readExternalCliBootstrapCredential;
function normalizeProviderScope(values: Iterable<string> | undefined): Set<string> | undefined {
if (values === undefined) {
return undefined;
}
const out = new Set<string>();
for (const value of values) {
const raw = value.trim();
if (!raw) {
continue;
}
out.add(raw.toLowerCase());
const normalized = normalizeProviderId(raw);
if (normalized) {
out.add(normalized);
}
}
return out;
}
function normalizeProfileScope(values: Iterable<string> | undefined): Set<string> | undefined {
if (values === undefined) {
return undefined;
}
const out = new Set<string>();
for (const value of values) {
const raw = value.trim().toLowerCase();
if (raw) {
out.add(raw);
}
}
return out;
}
function isExternalCliProviderInScope(
providerConfig: ExternalCliSyncProvider,
options?: ExternalCliAuthProfileOptions,
): boolean {
const providerScope = normalizeProviderScope(options?.providerIds);
const profileScope = normalizeProfileScope(options?.profileIds);
if (providerScope === undefined && profileScope === undefined) {
return true;
}
if (profileScope?.has(providerConfig.profileId.toLowerCase())) {
return true;
}
if (!providerScope || providerScope.size === 0) {
return false;
}
const aliases = [providerConfig.provider, ...(providerConfig.aliases ?? [])];
return aliases.some((alias) => {
const raw = alias.trim().toLowerCase();
const normalized = normalizeProviderId(alias);
return providerScope.has(raw) || (normalized ? providerScope.has(normalized) : false);
});
}
export function resolveExternalCliAuthProfiles(
store: AuthProfileStore,
options?: ExternalCliAuthProfileOptions,
): ExternalCliResolvedProfile[] {
const profiles: ExternalCliResolvedProfile[] = [];
const now = Date.now();
for (const providerConfig of EXTERNAL_CLI_SYNC_PROVIDERS) {
const creds = providerConfig.readCredentials();
if (!isExternalCliProviderInScope(providerConfig, options)) {
continue;
}
const creds = providerConfig.readCredentials({
allowKeychainPrompt: options?.allowKeychainPrompt,
});
if (!creds) {
continue;
}

View File

@@ -36,6 +36,8 @@ type LoadAuthProfileStoreOptions = {
allowKeychainPrompt?: boolean;
readOnly?: boolean;
syncExternalCli?: boolean;
externalCliProviderIds?: Iterable<string>;
externalCliProfileIds?: Iterable<string>;
};
type SaveAuthProfileStoreOptions = {
@@ -269,12 +271,20 @@ export function loadAuthProfileStoreForRuntime(
const authPath = resolveAuthStorePath(agentDir);
const mainAuthPath = resolveAuthStorePath();
if (!agentDir || authPath === mainAuthPath) {
return overlayExternalAuthProfiles(store, { agentDir });
return overlayExternalAuthProfiles(store, {
agentDir,
allowKeychainPrompt: options?.allowKeychainPrompt,
externalCliProviderIds: options?.externalCliProviderIds,
externalCliProfileIds: options?.externalCliProfileIds,
});
}
const mainStore = loadAuthProfileStoreForAgent(undefined, options);
return overlayExternalAuthProfiles(mergeAuthProfileStores(mainStore, store), {
agentDir,
allowKeychainPrompt: options?.allowKeychainPrompt,
externalCliProviderIds: options?.externalCliProviderIds,
externalCliProfileIds: options?.externalCliProfileIds,
});
}
@@ -297,11 +307,20 @@ export function loadAuthProfileStoreWithoutExternalProfiles(agentDir?: string):
export function ensureAuthProfileStore(
agentDir?: string,
options?: { allowKeychainPrompt?: boolean },
options?: {
allowKeychainPrompt?: boolean;
externalCliProviderIds?: Iterable<string>;
externalCliProfileIds?: Iterable<string>;
},
): AuthProfileStore {
return overlayExternalAuthProfiles(
ensureAuthProfileStoreWithoutExternalProfiles(agentDir, options),
{ agentDir },
{
agentDir,
allowKeychainPrompt: options?.allowKeychainPrompt,
externalCliProviderIds: options?.externalCliProviderIds,
externalCliProfileIds: options?.externalCliProfileIds,
},
);
}

View File

@@ -456,6 +456,7 @@ export async function resolveImplicitProviders(
const getAuthStore = () =>
(authStore ??= ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
externalCliProviderIds: params.providerDiscoveryProviderIds,
}));
const context: ImplicitProviderContext = {
...params,

View File

@@ -5,7 +5,11 @@ import type { GatewayRequestHandlerOptions } from "./types.js";
const mocks = vi.hoisted(() => ({
getRuntimeConfig: vi.fn(() => ({})),
resolveOpenClawAgentDir: vi.fn(() => "/tmp/agent"),
ensureAuthProfileStore: vi.fn(() => ({ profiles: {} })),
ensureAuthProfileStore: vi.fn((agentDir?: string, options?: unknown) => {
void agentDir;
void options;
return { profiles: {} };
}),
buildAuthHealthSummary: vi.fn(
(): AuthHealthSummary => ({ now: 0, warnAfterMs: 0, profiles: [], providers: [] }),
),
@@ -187,6 +191,50 @@ describe("models.authStatus", () => {
expect(mocks.loadProviderUsageSummary).not.toHaveBeenCalled();
});
it("scopes external CLI auth overlays to configured providers", async () => {
mocks.getRuntimeConfig.mockReturnValue({
auth: {
profiles: {
"opencode-go:default": { provider: "opencode-go", mode: "api_key" },
},
},
agents: {
defaults: {
model: { primary: "opencode-go/kimi-k2.6" },
},
},
models: {
providers: {
"opencode-go": {
baseUrl: "https://example.test/v1",
auth: "api-key",
models: [],
},
},
},
});
await handler(createOptions());
expect(mocks.ensureAuthProfileStore).toHaveBeenCalledWith(
"/tmp/agent",
expect.objectContaining({
externalCliProviderIds: expect.arrayContaining(["opencode-go"]),
externalCliProfileIds: ["opencode-go:default"],
}),
);
const [, options] = mocks.ensureAuthProfileStore.mock.calls[0] ?? [];
expect((options as { externalCliProviderIds?: string[] }).externalCliProviderIds).not.toContain(
"claude-cli",
);
});
it("keeps the auth store overlay unscoped when config has no provider signal", async () => {
await handler(createOptions());
expect(mocks.ensureAuthProfileStore).toHaveBeenCalledWith("/tmp/agent");
});
it("still returns providers when usage fetch fails", async () => {
mocks.buildAuthHealthSummary.mockReturnValue(createOpenAiCodexOauthHealthSummary());
mocks.loadProviderUsageSummary.mockRejectedValue(new Error("timeout"));

View File

@@ -8,6 +8,7 @@ import {
formatRemainingShort,
} from "../../agents/auth-health.js";
import { ensureAuthProfileStore } from "../../agents/auth-profiles.js";
import { resolveExternalCliAuthScopeFromConfig } from "../../agents/auth-profiles/external-cli-scope.js";
import { normalizeProviderId } from "../../agents/provider-id.js";
import type { OpenClawConfig } from "../../config/config.js";
import { isSecretRef } from "../../config/types.secrets.js";
@@ -292,7 +293,13 @@ export const modelsAuthStatusHandlers: GatewayRequestHandlers = {
try {
const cfg = context.getRuntimeConfig();
const agentDir = resolveOpenClawAgentDir();
const store = ensureAuthProfileStore(agentDir);
const externalCliAuthScope = resolveExternalCliAuthScopeFromConfig(cfg);
const store = externalCliAuthScope
? ensureAuthProfileStore(agentDir, {
externalCliProviderIds: externalCliAuthScope.providerIds,
externalCliProfileIds: externalCliAuthScope.profileIds,
})
: ensureAuthProfileStore(agentDir);
const configured = resolveConfiguredProviders(cfg);
const authHealth: AuthHealthSummary = buildAuthHealthSummary({
store,