From f79553bef61f7b28a848d0c7dd6b04db99503083 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 29 Apr 2026 12:23:50 +0100 Subject: [PATCH] fix(auth): scope external CLI auth status overlays (#74156) * fix(auth): scope external CLI auth status overlays * fix: pass external auth config to overlays * fix(auth): keep no-prompt CLI reads file-only * docs: update clawsweeper app wording --- .agents/skills/clawsweeper/SKILL.md | 4 +- CHANGELOG.md | 1 + docs/auth-credential-semantics.md | 8 ++ .../auth-profiles.external-cli-scope.test.ts | 5 +- .../auth-profiles.external-cli-sync.test.ts | 30 ++++ src/agents/auth-profiles/external-auth.ts | 7 +- .../auth-profiles/external-cli-scope.ts | 9 +- src/agents/auth-profiles/external-cli-sync.ts | 8 +- .../auth-profiles/external-oauth.test.ts | 25 +++- src/agents/auth-profiles/store.ts | 6 + src/agents/cli-credentials.test.ts | 129 +++++++++++++++++- src/agents/cli-credentials.ts | 27 ++-- .../server-methods/models-auth-status.test.ts | 12 +- .../server-methods/models-auth-status.ts | 12 +- 14 files changed, 251 insertions(+), 32 deletions(-) diff --git a/.agents/skills/clawsweeper/SKILL.md b/.agents/skills/clawsweeper/SKILL.md index 2e78b315af8..fc4a017238d 100644 --- a/.agents/skills/clawsweeper/SKILL.md +++ b/.agents/skills/clawsweeper/SKILL.md @@ -24,12 +24,12 @@ read-only report work read-only unless Peter asked to commit. ## One Bot, One App -Use the ClawSweeper repo and the `openclaw-ci` GitHub App. Use only +Use the ClawSweeper repo and the `clawsweeper` GitHub App. Use only `CLAWSWEEPER_*` configuration for this automation. Required app setup: -- `CLAWSWEEPER_APP_CLIENT_ID`: public app client ID for `openclaw-ci`. +- `CLAWSWEEPER_APP_CLIENT_ID`: public app client ID for `clawsweeper`. - `CLAWSWEEPER_APP_PRIVATE_KEY`: private key used only inside `actions/create-github-app-token` steps. - Target app permissions: read target scan context; write issues and pull diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b151f3c123..8ca36dd3caf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,6 +91,7 @@ Docs: https://docs.openclaw.ai - 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. - Gateway/sessions: expose effective agent runtime metadata on session rows, `sessions.patch`, and local `openclaw sessions --json`, while keeping Claude CLI-backed rows on the canonical model provider so runtime backend and model identity are no longer conflated. Fixes #73090. Thanks @vishutdhar. +- Gateway/auth status: scope external CLI credential overlays to configured providers, runtimes, or profiles and keep status reads off new Keychain prompts, so single-provider Gateway configs no longer probe unrelated Claude/Codex/MiniMax auth on startup. Fixes #73908. Thanks @Ailuras. - Agents/runtime status: expose effective agent runtime metadata in `agents.list`, Control UI agent panels, and `/agents`, and avoid rendering stale or cumulative CLI token totals as live context usage. Fixes #73660, #73578, and #45268. Thanks @spartman, @DashLabsDev, and @xyooz. - Agents/transcripts: strip empty assistant text blocks while preserving valid text, images, and signatures, so Anthropic-style providers no longer reject sanitized transcript turns. Fixes #73640. Thanks @jowhee327. - Providers/Bedrock: omit deprecated `temperature` for Claude Opus 4.7 Bedrock model ids, named and application inference profiles, including dotted `opus-4.7` refs, and classify the nested validation response for failover. Fixes #73663. Thanks @bstanbury. diff --git a/docs/auth-credential-semantics.md b/docs/auth-credential-semantics.md index 2027a1bc4c8..3c1f81ba371 100644 --- a/docs/auth-credential-semantics.md +++ b/docs/auth-credential-semantics.md @@ -80,6 +80,14 @@ the target agent signs in separately and creates its own local profile. candidate for it, `models status --probe` reports `status: no_model` with `reasonCode: no_model`. +## External CLI credential discovery + +- Runtime-only credentials owned by external CLIs are discovered only when the + provider, runtime, or auth profile is in scope for the current operation, or + when a stored local profile for that external source already exists. +- Read-only/status paths pass `allowKeychainPrompt: false`; they use file-backed + external CLI credentials only and do not read or reuse macOS Keychain results. + ## OAuth SecretRef Policy Guard - SecretRef input is for static credentials only. diff --git a/src/agents/auth-profiles.external-cli-scope.test.ts b/src/agents/auth-profiles.external-cli-scope.test.ts index feda6aaff47..a3db6d645bf 100644 --- a/src/agents/auth-profiles.external-cli-scope.test.ts +++ b/src/agents/auth-profiles.external-cli-scope.test.ts @@ -59,7 +59,7 @@ describe("external CLI auth scope", () => { { id: "worker", model: "opencode-go/kimi-k2.6", - agentRuntime: { id: "codex" }, + agentRuntime: { id: "codex-app-server" }, subagents: { model: { primary: "z.ai/glm-4.7" } }, }, ], @@ -75,11 +75,12 @@ describe("external CLI auth scope", () => { "openai-codex", "minimax-portal", "claude-cli", - "codex", + "codex-app-server", "opencode-go", "z.ai", "zai", ]), ); + expect(scope?.profileIds).toContain("openai-codex:default"); }); }); diff --git a/src/agents/auth-profiles.external-cli-sync.test.ts b/src/agents/auth-profiles.external-cli-sync.test.ts index 8e7f4952478..c408ae875c6 100644 --- a/src/agents/auth-profiles.external-cli-sync.test.ts +++ b/src/agents/auth-profiles.external-cli-sync.test.ts @@ -374,6 +374,36 @@ describe("external cli oauth resolution", () => { expect(mocks.readMiniMaxCliCredentialsCached).not.toHaveBeenCalled(); }); + it("passes non-prompting keychain policy to scoped Codex CLI credential reads", () => { + mocks.readCodexCliCredentialsCached.mockReturnValue( + makeOAuthCredential({ + provider: "openai-codex", + access: "codex-cli-access", + refresh: "codex-cli-refresh", + }), + ); + + const profiles = resolveExternalCliAuthProfiles(makeStore(), { + providerIds: ["codex-app-server"], + allowKeychainPrompt: false, + }); + + expect(profiles).toEqual([ + { + profileId: OPENAI_CODEX_DEFAULT_PROFILE_ID, + credential: expect.objectContaining({ + type: "oauth", + provider: "openai-codex", + }), + }, + ]); + expect(mocks.readCodexCliCredentialsCached).toHaveBeenCalledWith( + expect.objectContaining({ allowKeychainPrompt: false }), + ); + expect(mocks.readClaudeCliCredentialsCached).not.toHaveBeenCalled(); + expect(mocks.readMiniMaxCliCredentialsCached).not.toHaveBeenCalled(); + }); + it("ignores Claude CLI token credentials", () => { mocks.readClaudeCliCredentialsCached.mockReturnValue({ type: "token", diff --git a/src/agents/auth-profiles/external-auth.ts b/src/agents/auth-profiles/external-auth.ts index a72ea26dfed..b3e13beee5c 100644 --- a/src/agents/auth-profiles/external-auth.ts +++ b/src/agents/auth-profiles/external-auth.ts @@ -1,3 +1,4 @@ +import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { ProviderExternalAuthProfile } from "../../plugins/provider-external-auth.types.js"; import { resolveExternalAuthProfilesWithPlugins } from "../../plugins/provider-runtime.js"; import * as externalCliSync from "./external-cli-sync.js"; @@ -12,6 +13,7 @@ type ExternalAuthProfileMap = Map; type ResolveExternalAuthProfiles = typeof resolveExternalAuthProfilesWithPlugins; type ExternalCliOverlayOptions = { allowKeychainPrompt?: boolean; + config?: OpenClawConfig; externalCliProviderIds?: Iterable; externalCliProfileIds?: Iterable; }; @@ -50,8 +52,9 @@ function resolveExternalAuthProfileMap(params: { resolveExternalAuthProfilesForRuntime ?? resolveExternalAuthProfilesWithPlugins; const profiles = resolveProfiles({ env, + config: params.externalCli?.config, context: { - config: undefined, + config: params.externalCli?.config, agentDir: params.agentDir, workspaceDir: undefined, env, @@ -118,6 +121,7 @@ export function shouldPersistExternalAuthProfile(params: { credential: OAuthCredential; agentDir?: string; env?: NodeJS.ProcessEnv; + config?: OpenClawConfig; externalCliProviderIds?: Iterable; externalCliProfileIds?: Iterable; }): boolean { @@ -126,6 +130,7 @@ export function shouldPersistExternalAuthProfile(params: { agentDir: params.agentDir, env: params.env, externalCli: { + config: params.config, externalCliProviderIds: params.externalCliProviderIds, externalCliProfileIds: params.externalCliProfileIds, }, diff --git a/src/agents/auth-profiles/external-cli-scope.ts b/src/agents/auth-profiles/external-cli-scope.ts index 6d496a05522..aa0ef47541e 100644 --- a/src/agents/auth-profiles/external-cli-scope.ts +++ b/src/agents/auth-profiles/external-cli-scope.ts @@ -48,6 +48,7 @@ function addExternalCliRuntimeScope(out: Set, value: string | undefined) normalized === "claude-cli" || normalized === "codex" || normalized === "codex-cli" || + normalized === "codex-app-server" || normalized === "openai-codex" || normalized === "minimax" || normalized === "minimax-cli" || @@ -73,8 +74,14 @@ export function resolveExternalCliAuthScopeFromConfig( } addProviderScopeId(providerIds, profile?.provider); } - for (const provider of Object.keys(cfg.auth?.order ?? {})) { + for (const [provider, orderedProfileIds] of Object.entries(cfg.auth?.order ?? {})) { addProviderScopeId(providerIds, provider); + for (const profileId of orderedProfileIds ?? []) { + const normalizedProfileId = profileId.trim(); + if (normalizedProfileId) { + profileIds.add(normalizedProfileId); + } + } } const defaults = cfg.agents?.defaults; diff --git a/src/agents/auth-profiles/external-cli-sync.ts b/src/agents/auth-profiles/external-cli-sync.ts index 8a40b18f409..70cade2a39b 100644 --- a/src/agents/auth-profiles/external-cli-sync.ts +++ b/src/agents/auth-profiles/external-cli-sync.ts @@ -100,8 +100,12 @@ 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 }), + aliases: ["codex", "codex-cli", "codex-app-server"], + readCredentials: (options) => + readCodexCliCredentialsCached({ + ttlMs: EXTERNAL_CLI_SYNC_TTL_MS, + allowKeychainPrompt: options?.allowKeychainPrompt, + }), bootstrapOnly: true, }, { diff --git a/src/agents/auth-profiles/external-oauth.test.ts b/src/agents/auth-profiles/external-oauth.test.ts index e3908250296..a7dd746c6e5 100644 --- a/src/agents/auth-profiles/external-oauth.test.ts +++ b/src/agents/auth-profiles/external-oauth.test.ts @@ -11,7 +11,7 @@ const resolveExternalAuthProfilesWithPluginsMock = vi.fn< (params: unknown) => ProviderExternalAuthProfile[] >(() => []); const readCodexCliCredentialsCachedMock = vi.hoisted(() => - vi.fn<() => OAuthCredential | null>(() => null), + vi.fn<(_options?: unknown) => OAuthCredential | null>(() => null), ); vi.mock("../cli-credentials.js", () => ({ @@ -70,6 +70,29 @@ describe("auth external oauth helpers", () => { }); }); + it("passes config and CLI scope through overlay resolution", () => { + const cfg = { + models: { + providers: { "openai-codex": { auth: "oauth" as const, baseUrl: "", models: [] } }, + }, + }; + readCodexCliCredentialsCachedMock.mockReturnValueOnce(createCredential()); + + overlayExternalOAuthProfiles(createStore(), { + allowKeychainPrompt: false, + config: cfg, + externalCliProviderIds: ["openai-codex"], + }); + + expect(resolveExternalAuthProfilesWithPluginsMock).toHaveBeenCalledWith( + expect.objectContaining({ + config: cfg, + context: expect.objectContaining({ config: cfg }), + }), + ); + expect(readCodexCliCredentialsCachedMock).toHaveBeenCalledTimes(1); + }); + it("omits exact runtime-only overlays from persisted store writes", () => { const credential = createCredential(); resolveExternalAuthProfilesWithPluginsMock.mockReturnValueOnce([ diff --git a/src/agents/auth-profiles/store.ts b/src/agents/auth-profiles/store.ts index 66a3bb10c7c..515df101138 100644 --- a/src/agents/auth-profiles/store.ts +++ b/src/agents/auth-profiles/store.ts @@ -1,5 +1,6 @@ import fs from "node:fs"; import { isDeepStrictEqual } from "node:util"; +import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { withFileLock } from "../../infra/file-lock.js"; import { saveJsonFile } from "../../infra/json-file.js"; import { @@ -36,6 +37,7 @@ import type { AuthProfileStore } from "./types.js"; type LoadAuthProfileStoreOptions = { allowKeychainPrompt?: boolean; + config?: OpenClawConfig; readOnly?: boolean; syncExternalCli?: boolean; externalCliProviderIds?: Iterable; @@ -359,6 +361,7 @@ export function loadAuthProfileStoreForRuntime( return overlayExternalAuthProfiles(store, { agentDir, allowKeychainPrompt: options?.allowKeychainPrompt, + config: options?.config, externalCliProviderIds: options?.externalCliProviderIds, externalCliProfileIds: options?.externalCliProfileIds, }); @@ -368,6 +371,7 @@ export function loadAuthProfileStoreForRuntime( return overlayExternalAuthProfiles(mergeAuthProfileStores(mainStore, store), { agentDir, allowKeychainPrompt: options?.allowKeychainPrompt, + config: options?.config, externalCliProviderIds: options?.externalCliProviderIds, externalCliProfileIds: options?.externalCliProfileIds, }); @@ -394,6 +398,7 @@ export function ensureAuthProfileStore( agentDir?: string, options?: { allowKeychainPrompt?: boolean; + config?: OpenClawConfig; externalCliProviderIds?: Iterable; externalCliProfileIds?: Iterable; }, @@ -403,6 +408,7 @@ export function ensureAuthProfileStore( { agentDir, allowKeychainPrompt: options?.allowKeychainPrompt, + config: options?.config, externalCliProviderIds: options?.externalCliProviderIds, externalCliProfileIds: options?.externalCliProfileIds, }, diff --git a/src/agents/cli-credentials.test.ts b/src/agents/cli-credentials.test.ts index bc1f00a4cda..c330856d3e8 100644 --- a/src/agents/cli-credentials.test.ts +++ b/src/agents/cli-credentials.test.ts @@ -272,7 +272,7 @@ describe("cli credentials", () => { expect(execSyncMock).toHaveBeenCalledTimes(1); }); - it("reuses cached Claude keychain credentials for no-prompt reads", async () => { + it("keeps no-prompt Claude reads on the file credential path after a keychain read", async () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-claude-cache-")); vi.setSystemTime(new Date("2025-01-01T00:00:00Z")); mockClaudeCliCredentialRead(); @@ -292,7 +292,12 @@ describe("cli credentials", () => { execSync: execSyncMock, }); - expect(withoutPrompt).toEqual(withKeychain); + expect(withKeychain).toMatchObject({ + type: "oauth", + provider: "anthropic", + refresh: "cached-refresh", + }); + expect(withoutPrompt).toBeNull(); expect(execSyncMock).toHaveBeenCalledTimes(1); }); @@ -361,6 +366,126 @@ describe("cli credentials", () => { }); }); + it("does not read Codex keychain when keychain prompts are disabled", async () => { + const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-codex-no-prompt-")); + process.env.CODEX_HOME = tempHome; + const expSeconds = Math.floor(Date.parse("2026-03-24T12:34:56Z") / 1000); + const authPath = path.join(tempHome, "auth.json"); + fs.mkdirSync(tempHome, { recursive: true, mode: 0o700 }); + fs.writeFileSync( + authPath, + JSON.stringify({ + tokens: { + access_token: createJwtWithExp(expSeconds), + refresh_token: "file-refresh", + }, + }), + "utf8", + ); + + const creds = readCodexCliCredentialsCached({ + allowKeychainPrompt: false, + ttlMs: CLI_CREDENTIALS_CACHE_TTL_MS, + platform: "darwin", + execSync: execSyncMock, + }); + + expect(creds).toMatchObject({ + access: createJwtWithExp(expSeconds), + refresh: "file-refresh", + provider: "openai-codex", + }); + expect(execSyncMock).not.toHaveBeenCalled(); + }); + + it("does not let no-keychain Codex cache misses poison keychain reads", async () => { + const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-codex-cache-")); + process.env.CODEX_HOME = tempHome; + const expSeconds = Math.floor(Date.parse("2026-03-24T12:34:56Z") / 1000); + + const withoutKeychain = readCodexCliCredentialsCached({ + allowKeychainPrompt: false, + ttlMs: CLI_CREDENTIALS_CACHE_TTL_MS, + platform: "darwin", + execSync: execSyncMock, + }); + expect(withoutKeychain).toBeNull(); + + execSyncMock.mockReturnValue( + JSON.stringify({ + tokens: { + access_token: createJwtWithExp(expSeconds), + refresh_token: "keychain-refresh", + }, + }), + ); + const withKeychain = readCodexCliCredentialsCached({ + allowKeychainPrompt: true, + ttlMs: CLI_CREDENTIALS_CACHE_TTL_MS, + platform: "darwin", + execSync: execSyncMock, + }); + + expect(withKeychain).toMatchObject({ + access: createJwtWithExp(expSeconds), + refresh: "keychain-refresh", + provider: "openai-codex", + }); + expect(execSyncMock).toHaveBeenCalledTimes(1); + }); + + it("keeps no-prompt Codex reads on auth.json after a keychain read", async () => { + const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-codex-cache-")); + process.env.CODEX_HOME = tempHome; + const keychainExpiry = Math.floor(Date.parse("2026-03-24T12:34:56Z") / 1000); + const fileExpiry = Math.floor(Date.parse("2026-03-25T12:34:56Z") / 1000); + const authPath = path.join(tempHome, "auth.json"); + fs.mkdirSync(tempHome, { recursive: true, mode: 0o700 }); + fs.writeFileSync( + authPath, + JSON.stringify({ + tokens: { + access_token: createJwtWithExp(fileExpiry), + refresh_token: "file-refresh", + }, + }), + "utf8", + ); + execSyncMock.mockReturnValue( + JSON.stringify({ + tokens: { + access_token: createJwtWithExp(keychainExpiry), + refresh_token: "keychain-refresh", + }, + }), + ); + + const withKeychain = readCodexCliCredentialsCached({ + allowKeychainPrompt: true, + ttlMs: CLI_CREDENTIALS_CACHE_TTL_MS, + platform: "darwin", + execSync: execSyncMock, + }); + const withoutPrompt = readCodexCliCredentialsCached({ + allowKeychainPrompt: false, + ttlMs: CLI_CREDENTIALS_CACHE_TTL_MS, + platform: "darwin", + execSync: execSyncMock, + }); + + expect(withKeychain).toMatchObject({ + refresh: "keychain-refresh", + expires: keychainExpiry * 1000, + provider: "openai-codex", + }); + expect(withoutPrompt).toMatchObject({ + refresh: "file-refresh", + expires: fileExpiry * 1000, + provider: "openai-codex", + }); + expect(execSyncMock).toHaveBeenCalledTimes(1); + }); + it("invalidates cached Codex credentials when auth.json changes within the TTL window", () => { const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-codex-cache-")); process.env.CODEX_HOME = tempHome; diff --git a/src/agents/cli-credentials.ts b/src/agents/cli-credentials.ts index fe6ba1c29b8..3d8adbb4719 100644 --- a/src/agents/cli-credentials.ts +++ b/src/agents/cli-credentials.ts @@ -249,9 +249,10 @@ function readCodexKeychainAuthRecord(options?: { codexHome?: string; platform?: NodeJS.Platform; execSync?: ExecSyncFn; + allowKeychainPrompt?: boolean; }): Record | null { const { platform, execSyncImpl, codexHome } = resolveCodexKeychainParams(options); - if (platform !== "darwin") { + if (platform !== "darwin" || options?.allowKeychainPrompt === false) { return null; } const account = computeCodexKeychainAccount(codexHome); @@ -277,6 +278,7 @@ function readCodexKeychainCredentials(options?: { codexHome?: string; platform?: NodeJS.Platform; execSync?: ExecSyncFn; + allowKeychainPrompt?: boolean; }): CodexCliCredential | null { const parsed = readCodexKeychainAuthRecord(options); if (!parsed) { @@ -458,17 +460,6 @@ export function readClaudeCliCredentialsCached(options?: { const platform = options?.platform ?? process.platform; const ttlMs = options?.ttlMs ?? 0; const credentialsPath = resolveClaudeCliCredentialsPath(options?.homeDir); - const keychainCacheKey = `${credentialsPath}:keychain`; - if ( - ttlMs > 0 && - platform === "darwin" && - options?.allowKeychainPrompt === false && - claudeCliCache?.cacheKey === keychainCacheKey && - claudeCliCache.value && - Date.now() - claudeCliCache.readAt < ttlMs - ) { - return claudeCliCache.value; - } const keychainIntent = platform === "darwin" && options?.allowKeychainPrompt !== false ? "keychain" : "file"; return readCachedCliCredential({ @@ -608,11 +599,13 @@ export function writeClaudeCliCredentials( export function readCodexCliCredentials(options?: { codexHome?: string; + allowKeychainPrompt?: boolean; platform?: NodeJS.Platform; execSync?: ExecSyncFn; }): CodexCliCredential | null { const keychain = readCodexKeychainCredentials({ codexHome: options?.codexHome, + allowKeychainPrompt: options?.allowKeychainPrompt, platform: options?.platform, execSync: options?.execSync, }); @@ -664,18 +657,24 @@ export function readCodexCliCredentials(options?: { export function readCodexCliCredentialsCached(options?: { codexHome?: string; + allowKeychainPrompt?: boolean; ttlMs?: number; platform?: NodeJS.Platform; execSync?: ExecSyncFn; }): CodexCliCredential | null { + const platform = options?.platform ?? process.platform; + const ttlMs = options?.ttlMs ?? 0; const authPath = path.join(resolveCodexHomePath(options?.codexHome), CODEX_CLI_AUTH_FILENAME); + const keychainIntent = + platform === "darwin" && options?.allowKeychainPrompt !== false ? "keychain" : "file"; return readCachedCliCredential({ - ttlMs: options?.ttlMs ?? 0, + ttlMs, cache: codexCliCache, - cacheKey: `${options?.platform ?? process.platform}|${authPath}`, + cacheKey: `${platform}|${authPath}:${keychainIntent}`, read: () => readCodexCliCredentials({ codexHome: options?.codexHome, + allowKeychainPrompt: options?.allowKeychainPrompt, platform: options?.platform, execSync: options?.execSync, }), diff --git a/src/gateway/server-methods/models-auth-status.test.ts b/src/gateway/server-methods/models-auth-status.test.ts index 42f6af1680a..446709a9b64 100644 --- a/src/gateway/server-methods/models-auth-status.test.ts +++ b/src/gateway/server-methods/models-auth-status.test.ts @@ -219,6 +219,8 @@ describe("models.authStatus", () => { expect(mocks.ensureAuthProfileStore).toHaveBeenCalledWith( "/tmp/agent", expect.objectContaining({ + allowKeychainPrompt: false, + config: expect.any(Object), externalCliProviderIds: expect.arrayContaining(["opencode-go"]), externalCliProfileIds: ["opencode-go:default"], }), @@ -232,7 +234,15 @@ describe("models.authStatus", () => { it("keeps the auth store overlay unscoped when config has no provider signal", async () => { await handler(createOptions()); - expect(mocks.ensureAuthProfileStore).toHaveBeenCalledWith("/tmp/agent"); + expect(mocks.ensureAuthProfileStore).toHaveBeenCalledWith( + "/tmp/agent", + expect.objectContaining({ + allowKeychainPrompt: false, + config: expect.any(Object), + externalCliProviderIds: undefined, + externalCliProfileIds: undefined, + }), + ); }); it("still returns providers when usage fetch fails", async () => { diff --git a/src/gateway/server-methods/models-auth-status.ts b/src/gateway/server-methods/models-auth-status.ts index 9a471a74849..1017bfaed54 100644 --- a/src/gateway/server-methods/models-auth-status.ts +++ b/src/gateway/server-methods/models-auth-status.ts @@ -294,12 +294,12 @@ export const modelsAuthStatusHandlers: GatewayRequestHandlers = { const cfg = context.getRuntimeConfig(); const agentDir = resolveOpenClawAgentDir(); const externalCliAuthScope = resolveExternalCliAuthScopeFromConfig(cfg); - const store = externalCliAuthScope - ? ensureAuthProfileStore(agentDir, { - externalCliProviderIds: externalCliAuthScope.providerIds, - externalCliProfileIds: externalCliAuthScope.profileIds, - }) - : ensureAuthProfileStore(agentDir); + const store = ensureAuthProfileStore(agentDir, { + allowKeychainPrompt: false, + config: cfg, + externalCliProviderIds: externalCliAuthScope?.providerIds, + externalCliProfileIds: externalCliAuthScope?.profileIds, + }); const configured = resolveConfiguredProviders(cfg); const authHealth: AuthHealthSummary = buildAuthHealthSummary({ store,