From 6afb08f9abf28102e5c94db4df4e8105aa07b46d Mon Sep 17 00:00:00 2001 From: Shakker Date: Mon, 15 Jun 2026 18:54:37 +0100 Subject: [PATCH] fix: persist Gemini CLI auth homes --- extensions/google/cli-backend.ts | 160 ++++++++++++++++---------- extensions/google/setup-api.test.ts | 96 +++++++++++++--- src/agents/cli-runner/prepare.test.ts | 89 ++++++++++++++ src/agents/cli-runner/prepare.ts | 16 +-- 4 files changed, 272 insertions(+), 89 deletions(-) diff --git a/extensions/google/cli-backend.ts b/extensions/google/cli-backend.ts index 769b0e06c336..9a3249ace39c 100644 --- a/extensions/google/cli-backend.ts +++ b/extensions/google/cli-backend.ts @@ -1,3 +1,4 @@ +import crypto from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; import type { CliBackendPlugin } from "openclaw/plugin-sdk/cli-backend"; @@ -5,7 +6,6 @@ import { CLI_FRESH_WATCHDOG_DEFAULTS, CLI_RESUME_WATCHDOG_DEFAULTS, } from "openclaw/plugin-sdk/cli-backend"; -import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path"; const GEMINI_MODEL_ALIASES: Record = { pro: "gemini-3.1-pro-preview", @@ -15,10 +15,12 @@ const GEMINI_MODEL_ALIASES: Record = { const GEMINI_CLI_DEFAULT_MODEL_REF = "google-gemini-cli/gemini-3-flash-preview"; const GEMINI_CLI_PROVIDER_ID = "google-gemini-cli"; const VERCEL_AI_GATEWAY_PROVIDER_ID = "vercel-ai-gateway"; +const GEMINI_CLI_CREDENTIALS_FILENAME = "gemini-credentials.json"; const GEMINI_CLI_GCA_AUTH_ENV = [ "GOOGLE_GENAI_USE_GCA", "GOOGLE_CLOUD_ACCESS_TOKEN", "GEMINI_FORCE_ENCRYPTED_FILE_STORAGE", + "GEMINI_FORCE_FILE_STORAGE", ]; const GEMINI_CLI_API_KEY_AUTH_ENV = [ ...GEMINI_CLI_GCA_AUTH_ENV, @@ -31,11 +33,11 @@ const GEMINI_CLI_API_KEY_AUTH_ENV = [ "GEMINI_CLI_CUSTOM_HEADERS", "GEMINI_API_KEY_AUTH_MECHANISM", ]; +const GEMINI_CLI_PROFILE_AUTH_ENV = [...GEMINI_CLI_API_KEY_AUTH_ENV, "GEMINI_API_KEY"]; type PreparedGeminiCliExecution = { env: Record; clearEnv: string[]; - cleanup: () => Promise; }; function normalizeString(value: string | undefined): string | undefined { @@ -68,6 +70,11 @@ type GeminiApiKeyCredential = GeminiAuthProfileCredential & { key: string; }; +type GeminiCliAuthHomeContext = { + agentDir?: string; + authProfileId?: string; +}; + function throwUnsupportedGeminiCredential(credential: GeminiAuthProfileCredential): never { if (credential.provider === VERCEL_AI_GATEWAY_PROVIDER_ID) { throw new Error( @@ -140,25 +147,58 @@ function requireGeminiApiKeyCredential( }; } -async function createIsolatedGeminiCliHome(settings: unknown): Promise<{ - tempHome: string; +function resolveGeminiCliProfileHome(ctx: GeminiCliAuthHomeContext): { + home: string; + geminiDir: string; +} { + const agentDir = normalizeString(ctx.agentDir); + if (!agentDir) { + throw new Error("Gemini CLI auth profile execution requires an agent directory."); + } + const authProfileId = normalizeString(ctx.authProfileId); + if (!authProfileId) { + throw new Error("Gemini CLI auth profile execution requires a selected auth profile."); + } + + const profileHash = crypto.createHash("sha256").update(authProfileId).digest("hex").slice(0, 24); + const home = path.join(agentDir, "cli-runtimes", GEMINI_CLI_PROVIDER_ID, "profiles", profileHash); + return { home, geminiDir: path.join(home, ".gemini") }; +} + +async function writeGeminiCliJson(filePath: string, value: unknown): Promise { + await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, { + encoding: "utf8", + mode: 0o600, + }); + await fs.chmod(filePath, 0o600); +} + +async function prepareGeminiCliProfileHome( + ctx: GeminiCliAuthHomeContext, + settings: unknown, +): Promise<{ + home: string; geminiDir: string; }> { - const tempHome = await fs.mkdtemp( - path.join(resolvePreferredOpenClawTmpDir(), "gemini-cli-home-"), - ); - await fs.chmod(tempHome, 0o700); - const geminiDir = path.join(tempHome, ".gemini"); + const { home, geminiDir } = resolveGeminiCliProfileHome(ctx); await fs.mkdir(geminiDir, { recursive: true, mode: 0o700 }); - await fs.writeFile( - path.join(geminiDir, "settings.json"), - `${JSON.stringify(settings, null, 2)}\n`, - { encoding: "utf8", mode: 0o600 }, - ); - return { tempHome, geminiDir }; + await fs.chmod(home, 0o700); + await fs.chmod(geminiDir, 0o700); + await Promise.all([ + writeGeminiCliJson(path.join(geminiDir, "settings.json"), settings), + writeGeminiCliJson(path.join(home, "settings.json"), settings), + ]); + return { home, geminiDir }; +} + +async function clearGeminiCliCachedCredentials(geminiDir: string): Promise { + // Gemini prefers its token store over oauth_creds.json. Rebuild that store + // from the selected OpenClaw profile each run so stale CLI auth cannot win. + await fs.rm(path.join(geminiDir, GEMINI_CLI_CREDENTIALS_FILENAME), { force: true }); } async function prepareGeminiCliOAuthHome( + ctx: GeminiCliAuthHomeContext, credential: GeminiAuthProfileCredential | undefined, ): Promise { const oauth = requireGeminiOAuthCredential(credential); @@ -166,43 +206,34 @@ async function prepareGeminiCliOAuthHome( return null; } - const { tempHome, geminiDir } = await createIsolatedGeminiCliHome({ + const { home, geminiDir } = await prepareGeminiCliProfileHome(ctx, { security: { auth: { selectedType: "oauth-personal" } }, }); - try { - const idToken = normalizeString(oauth.idToken); - const oauthCreds: Record = { - access_token: oauth.access, - refresh_token: oauth.refresh, - expiry_date: oauth.expires, - token_type: "Bearer", - }; - if (idToken) { - oauthCreds.id_token = idToken; - } - - await fs.writeFile( - path.join(geminiDir, "oauth_creds.json"), - `${JSON.stringify(oauthCreds, null, 2)}\n`, - { encoding: "utf8", mode: 0o600 }, - ); - - return { - env: { - GEMINI_CLI_HOME: tempHome, - }, - clearEnv: [...GEMINI_CLI_GCA_AUTH_ENV], - cleanup: async () => { - await fs.rm(tempHome, { recursive: true, force: true }); - }, - }; - } catch (error) { - await fs.rm(tempHome, { recursive: true, force: true }); - throw error; + await clearGeminiCliCachedCredentials(geminiDir); + const idToken = normalizeString(oauth.idToken); + const oauthCreds: Record = { + access_token: oauth.access, + refresh_token: oauth.refresh, + expiry_date: oauth.expires, + token_type: "Bearer", + }; + if (idToken) { + oauthCreds.id_token = idToken; } + + await writeGeminiCliJson(path.join(geminiDir, "oauth_creds.json"), oauthCreds); + + return { + env: { + GEMINI_CLI_HOME: home, + GEMINI_FORCE_FILE_STORAGE: "true", + }, + clearEnv: [...GEMINI_CLI_PROFILE_AUTH_ENV], + }; } async function prepareGeminiCliApiKeyHome( + ctx: GeminiCliAuthHomeContext, credential: GeminiAuthProfileCredential | undefined, ): Promise { const apiKey = requireGeminiApiKeyCredential(credential); @@ -210,31 +241,30 @@ async function prepareGeminiCliApiKeyHome( return null; } - const { tempHome } = await createIsolatedGeminiCliHome({ + const { home, geminiDir } = await prepareGeminiCliProfileHome(ctx, { security: { auth: { selectedType: "gemini-api-key" } }, }); - try { - return { - env: { - GEMINI_CLI_HOME: tempHome, - GEMINI_API_KEY: apiKey.key, - }, - clearEnv: [...GEMINI_CLI_API_KEY_AUTH_ENV], - cleanup: async () => { - await fs.rm(tempHome, { recursive: true, force: true }); - }, - }; - } catch (error) { - await fs.rm(tempHome, { recursive: true, force: true }); - throw error; - } + await Promise.all([ + fs.rm(path.join(geminiDir, "oauth_creds.json"), { force: true }), + clearGeminiCliCachedCredentials(geminiDir), + ]); + return { + env: { + GEMINI_CLI_HOME: home, + GEMINI_FORCE_FILE_STORAGE: "true", + GEMINI_API_KEY: apiKey.key, + }, + clearEnv: [...GEMINI_CLI_PROFILE_AUTH_ENV], + }; } async function prepareGeminiCliAuthHome( + ctx: GeminiCliAuthHomeContext, credential: GeminiAuthProfileCredential | undefined, ): Promise { return ( - (await prepareGeminiCliOAuthHome(credential)) ?? (await prepareGeminiCliApiKeyHome(credential)) + (await prepareGeminiCliOAuthHome(ctx, credential)) ?? + (await prepareGeminiCliApiKeyHome(ctx, credential)) ); } @@ -257,6 +287,10 @@ export function buildGoogleGeminiCliBackend(): CliBackendPlugin { authEpochMode: "profile-only", prepareExecution: async (ctx) => await prepareGeminiCliAuthHome( + { + agentDir: ctx.agentDir, + authProfileId: ctx.authProfileId, + }, (ctx as typeof ctx & { authCredential?: GeminiAuthProfileCredential }).authCredential, ), config: { diff --git a/extensions/google/setup-api.test.ts b/extensions/google/setup-api.test.ts index 8c2ba172cf16..ac99631f63d7 100644 --- a/extensions/google/setup-api.test.ts +++ b/extensions/google/setup-api.test.ts @@ -23,8 +23,10 @@ type GeminiPrepareContext = Parameters< }; function buildGeminiOAuthPrepareContext(workspaceDir: string): GeminiPrepareContext { + const agentDir = path.join(workspaceDir, "agent"); return { workspaceDir, + agentDir, provider: "google-gemini-cli", modelId: "gemini-3.1-pro-preview", authProfileId: "google-gemini-cli:user@example.test", @@ -42,8 +44,10 @@ function buildGeminiOAuthPrepareContext(workspaceDir: string): GeminiPrepareCont } function buildGeminiApiKeyPrepareContext(workspaceDir: string): GeminiPrepareContext { + const agentDir = path.join(workspaceDir, "agent"); return { workspaceDir, + agentDir, provider: "google-gemini-cli", modelId: "gemini-3.1-flash-lite", authProfileId: "google-gemini-cli:api-key", @@ -85,20 +89,20 @@ describe("google setup entry", () => { }); describe("google gemini cli backend auth bridge", () => { - it("materializes selected OpenClaw OAuth credentials into an isolated Gemini CLI home", async () => { + it("materializes selected OpenClaw OAuth credentials into a persistent profile-scoped Gemini CLI home", async () => { const backend = buildGoogleGeminiCliBackend(); const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-workspace-")); - let prepared: - | Awaited>> - | null - | undefined; let home: string | undefined; try { - prepared = await backend.prepareExecution?.(buildGeminiOAuthPrepareContext(workspaceDir)); + const context = buildGeminiOAuthPrepareContext(workspaceDir); + const prepared = await backend.prepareExecution?.(context); home = prepared?.env?.GEMINI_CLI_HOME; expect(home).toBeTruthy(); + expect(prepared?.env?.GEMINI_FORCE_FILE_STORAGE).toBe("true"); + expect(home).toContain(path.join(context.agentDir, "cli-runtimes", "google-gemini-cli")); + expect(home).not.toContain("user@example.test"); const raw = await fs.readFile(path.join(home ?? "", ".gemini", "oauth_creds.json"), "utf8"); expect(JSON.parse(raw)).toEqual({ @@ -108,29 +112,54 @@ describe("google gemini cli backend auth bridge", () => { expiry_date: 1_800_000_000_000, token_type: "Bearer", }); + const nestedSettingsRaw = await fs.readFile( + path.join(home ?? "", ".gemini", "settings.json"), + "utf8", + ); + const rootSettingsRaw = await fs.readFile(path.join(home ?? "", "settings.json"), "utf8"); + expect(JSON.parse(nestedSettingsRaw)).toEqual({ + security: { auth: { selectedType: "oauth-personal" } }, + }); + expect(JSON.parse(rootSettingsRaw)).toEqual(JSON.parse(nestedSettingsRaw)); + + const sessionMarker = path.join(home ?? "", ".gemini", "session-state.json"); + await fs.writeFile(sessionMarker, '{"keep":true}\n', "utf8"); + const cachedCredentialsPath = path.join(home ?? "", ".gemini", "gemini-credentials.json"); + await fs.writeFile(cachedCredentialsPath, "stale-cache", "utf8"); + + const preparedAgain = await backend.prepareExecution?.(context); + expect(preparedAgain?.env?.GEMINI_CLI_HOME).toBe(home); + await expect(fs.access(sessionMarker)).resolves.toBeUndefined(); + await expect(fs.access(cachedCredentialsPath)).rejects.toThrow(); } finally { - await prepared?.cleanup?.(); await fs.rm(workspaceDir, { recursive: true, force: true }); } - - await expect(fs.access(home ?? "")).rejects.toThrow(); }); - it("prepares selected Gemini API-key credentials without writing OAuth state", async () => { + it("prepares selected Gemini API-key credentials and removes stale OAuth state for that profile home", async () => { const backend = buildGoogleGeminiCliBackend(); const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-workspace-")); - let prepared: - | Awaited>> - | null - | undefined; let home: string | undefined; try { - prepared = await backend.prepareExecution?.(buildGeminiApiKeyPrepareContext(workspaceDir)); + const context = buildGeminiApiKeyPrepareContext(workspaceDir); + const firstPrepared = await backend.prepareExecution?.(context); + home = firstPrepared?.env?.GEMINI_CLI_HOME; + expect(home).toBeTruthy(); + await fs.writeFile(path.join(home ?? "", ".gemini", "oauth_creds.json"), "{}\n", "utf8"); + await fs.writeFile( + path.join(home ?? "", ".gemini", "gemini-credentials.json"), + "stale-cache", + "utf8", + ); + + const prepared = await backend.prepareExecution?.(context); home = prepared?.env?.GEMINI_CLI_HOME; expect(home).toBeTruthy(); expect(prepared?.env?.GEMINI_API_KEY).toBe("gemini-api-key"); + expect(prepared?.env?.GEMINI_FORCE_FILE_STORAGE).toBe("true"); + expect(prepared?.clearEnv).toContain("GEMINI_API_KEY"); expect(prepared?.clearEnv).toContain("GOOGLE_GENAI_USE_GCA"); expect(prepared?.clearEnv).toContain("GOOGLE_GENAI_USE_VERTEXAI"); expect(prepared?.clearEnv).toContain("GOOGLE_GEMINI_BASE_URL"); @@ -145,12 +174,12 @@ describe("google gemini cli backend auth bridge", () => { await expect( fs.access(path.join(home ?? "", ".gemini", "oauth_creds.json")), ).rejects.toThrow(); + await expect( + fs.access(path.join(home ?? "", ".gemini", "gemini-credentials.json")), + ).rejects.toThrow(); } finally { - await prepared?.cleanup?.(); await fs.rm(workspaceDir, { recursive: true, force: true }); } - - await expect(fs.access(home ?? "")).rejects.toThrow(); }); it("rejects Vercel AI Gateway profiles for the Gemini CLI backend", async () => { @@ -161,6 +190,7 @@ describe("google gemini cli backend auth bridge", () => { await expect( backend.prepareExecution?.({ workspaceDir, + agentDir: path.join(workspaceDir, "agent"), provider: "google-gemini-cli", modelId: "gemini-3.1-flash-lite", authProfileId: "vercel-ai-gateway:default", @@ -176,12 +206,14 @@ describe("google gemini cli backend auth bridge", () => { } }); - it("clears inherited Gemini GCA credentials when staging selected OAuth credentials", async () => { + it("clears inherited Gemini auth credentials when staging selected OAuth credentials", async () => { const backend = buildGoogleGeminiCliBackend(); const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-workspace-")); const originalUseGca = process.env.GOOGLE_GENAI_USE_GCA; const originalCloudAccessToken = process.env.GOOGLE_CLOUD_ACCESS_TOKEN; const originalForceEncryptedFileStorage = process.env.GEMINI_FORCE_ENCRYPTED_FILE_STORAGE; + const originalGeminiApiKey = process.env.GEMINI_API_KEY; + const originalGoogleApiKey = process.env.GOOGLE_API_KEY; let prepared: | Awaited>> | null @@ -190,6 +222,8 @@ describe("google gemini cli backend auth bridge", () => { process.env.GOOGLE_GENAI_USE_GCA = "true"; process.env.GOOGLE_CLOUD_ACCESS_TOKEN = "ambient-cloud-token"; process.env.GEMINI_FORCE_ENCRYPTED_FILE_STORAGE = "true"; + process.env.GEMINI_API_KEY = "ambient-gemini-key"; + process.env.GOOGLE_API_KEY = "ambient-google-key"; try { prepared = await backend.prepareExecution?.(buildGeminiOAuthPrepareContext(workspaceDir)); @@ -199,16 +233,40 @@ describe("google gemini cli backend auth bridge", () => { "GOOGLE_GENAI_USE_GCA", "GOOGLE_CLOUD_ACCESS_TOKEN", "GEMINI_FORCE_ENCRYPTED_FILE_STORAGE", + "GEMINI_FORCE_FILE_STORAGE", + "GOOGLE_GENAI_USE_VERTEXAI", + "GOOGLE_API_KEY", + "GOOGLE_CLOUD_PROJECT", + "GOOGLE_CLOUD_PROJECT_ID", + "GOOGLE_CLOUD_LOCATION", + "GOOGLE_GEMINI_BASE_URL", + "GEMINI_CLI_CUSTOM_HEADERS", + "GEMINI_API_KEY_AUTH_MECHANISM", + "GEMINI_API_KEY", ]); } finally { restoreEnv("GOOGLE_GENAI_USE_GCA", originalUseGca); restoreEnv("GOOGLE_CLOUD_ACCESS_TOKEN", originalCloudAccessToken); restoreEnv("GEMINI_FORCE_ENCRYPTED_FILE_STORAGE", originalForceEncryptedFileStorage); + restoreEnv("GEMINI_API_KEY", originalGeminiApiKey); + restoreEnv("GOOGLE_API_KEY", originalGoogleApiKey); await prepared?.cleanup?.(); await fs.rm(workspaceDir, { recursive: true, force: true }); } }); + it("requires an agent directory for profile-owned Gemini CLI state", async () => { + const backend = buildGoogleGeminiCliBackend(); + const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-workspace-")); + + try { + const { agentDir: _agentDir, ...context } = buildGeminiOAuthPrepareContext(workspaceDir); + await expect(backend.prepareExecution?.(context)).rejects.toThrow(/agent directory/); + } finally { + await fs.rm(workspaceDir, { recursive: true, force: true }); + } + }); + it("uses profile-only auth epochs for the private Gemini CLI bridge", () => { const backend = buildGoogleGeminiCliBackend(); diff --git a/src/agents/cli-runner/prepare.test.ts b/src/agents/cli-runner/prepare.test.ts index 6cfe332c546c..67c18f290328 100644 --- a/src/agents/cli-runner/prepare.test.ts +++ b/src/agents/cli-runner/prepare.test.ts @@ -22,6 +22,8 @@ import { createTestRegistry, } from "../../test-utils/channel-plugins.js"; import { captureEnv, setTestEnvValue } from "../../test-utils/env.js"; +import { resolveApiKeyForProfile as resolveApiKeyForProfileImpl } from "../auth-profiles/oauth.js"; +import { saveAuthProfileStore } from "../auth-profiles/store.js"; import { testing as cliBackendsTesting } from "../cli-backends.js"; import { hashCliSessionText } from "../cli-session.js"; import { resetContextWindowCacheForTest } from "../context.js"; @@ -263,6 +265,7 @@ describe("shouldSkipLocalCliCredentialEpoch", () => { args: [], cleanup: vi.fn(async () => undefined), })), + resolveApiKeyForProfile: resolveApiKeyForProfileImpl, }); mockGetGlobalHookRunner.mockReturnValue(null); getRuntimeConfigMock.mockReturnValue({}); @@ -321,6 +324,92 @@ describe("shouldSkipLocalCliCredentialEpoch", () => { ).toBe(false); }); + it("passes raw refreshed OAuth profile fields to profile-owned CLI preparation", async () => { + const { dir, sessionFile } = createSessionFile(); + const agentDir = path.join(dir, "agents", "main", "agent"); + const authProfileId = "google-gemini-cli:user@example.test"; + const prepareExecution = vi.fn(async () => ({ + env: { GEMINI_CLI_HOME: path.join(agentDir, "gemini-home") }, + })); + const resolveApiKeyForProfile = vi.fn(async () => ({ + apiKey: JSON.stringify({ token: "provider-formatted-access", projectId: "project-1" }), + provider: "google-gemini-cli", + email: "user@example.test", + })); + fs.mkdirSync(agentDir, { recursive: true }); + saveAuthProfileStore( + { + version: 1, + profiles: { + [authProfileId]: { + type: "oauth", + provider: "google-gemini-cli", + access: "raw-access-token", + refresh: "raw-refresh-token", + expires: 1_800_000_000_000, + projectId: "project-1", + email: "user@example.test", + }, + }, + }, + agentDir, + ); + cliBackendsTesting.setDepsForTest({ + resolvePluginSetupCliBackend: () => undefined, + resolveRuntimeCliBackends: () => [ + { + id: "google-gemini-cli", + pluginId: "google", + bundleMcp: false, + authEpochMode: "profile-only", + prepareExecution, + config: { + command: "gemini", + args: ["--prompt", "{prompt}"], + output: "json", + input: "arg", + sessionMode: "existing", + }, + }, + ], + }); + setCliRunnerPrepareTestDeps({ + resolveApiKeyForProfile, + }); + + try { + await prepareCliRunContext({ + sessionId: "session-test", + sessionKey: "agent:main:main", + sessionFile, + workspaceDir: dir, + prompt: "latest ask", + provider: "google-gemini-cli", + model: "gemini-3.1-pro-preview", + timeoutMs: 1_000, + runId: "run-test-gemini-oauth-raw-profile-fields", + authProfileId, + config: {}, + }); + + expect(resolveApiKeyForProfile).toHaveBeenCalledOnce(); + expect(prepareExecution).toHaveBeenCalledWith( + expect.objectContaining({ + authProfileId, + authCredential: expect.objectContaining({ + type: "oauth", + provider: "google-gemini-cli", + access: "raw-access-token", + refresh: "raw-refresh-token", + expires: 1_800_000_000_000, + }), + }), + ); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + it("prepares side questions without agent-turn context, tools, hooks, or reusable sessions", async () => { const { dir, sessionFile } = createSessionFile(); appendTranscriptEntry(sessionFile, { diff --git a/src/agents/cli-runner/prepare.ts b/src/agents/cli-runner/prepare.ts index 109de0926fec..427633531eb5 100644 --- a/src/agents/cli-runner/prepare.ts +++ b/src/agents/cli-runner/prepare.ts @@ -106,6 +106,7 @@ const prepareDeps = { prepareClaudeCliSkillsPlugin, claudeCliSessionTranscriptHasContent, claudeCliSessionTranscriptHasOrphanedToolUse, + resolveApiKeyForProfile, }; async function resolveCliSkillsPrompt(params: { @@ -314,7 +315,7 @@ export async function prepareCliRunContext( profileId: authProfileId, }), }); - const resolvedAuth = await resolveApiKeyForProfile({ + const resolvedAuth = await prepareDeps.resolveApiKeyForProfile({ cfg: params.config, store: writableAuthStore, profileId: authProfileId, @@ -329,12 +330,13 @@ export async function prepareCliRunContext( }); authCredential = authStore.profiles[authProfileId]; if (resolvedAuth && authCredential) { - authCredential = - authCredential.type === "api_key" - ? { ...authCredential, key: resolvedAuth.apiKey } - : authCredential.type === "token" - ? { ...authCredential, token: resolvedAuth.apiKey } - : { ...authCredential, access: resolvedAuth.apiKey }; + // Apply resolved strings only to static credentials with secret refs. + // OAuth CLI bridges need raw refreshed fields from the reloaded store. + if (authCredential.type === "api_key") { + authCredential = { ...authCredential, key: resolvedAuth.apiKey }; + } else if (authCredential.type === "token") { + authCredential = { ...authCredential, token: resolvedAuth.apiKey }; + } } } const extraSystemPrompt = params.extraSystemPrompt?.trim() ?? "";