diff --git a/src/agents/auth-profiles/profiles.ts b/src/agents/auth-profiles/profiles.ts index ecf75c8debb..58c0c48e10f 100644 --- a/src/agents/auth-profiles/profiles.ts +++ b/src/agents/auth-profiles/profiles.ts @@ -3,7 +3,7 @@ import { normalizeSecretInput } from "../../utils/normalize-secret-input.js"; import { resolveProviderIdForAuth } from "../provider-auth-aliases.js"; import { normalizeProviderId } from "../provider-id.js"; import { - ensureAuthProfileStore, + ensureAuthProfileStoreForLocalUpdate, saveAuthProfileStore, updateAuthProfileStoreWithLock, } from "./store.js"; @@ -59,9 +59,9 @@ export function upsertAuthProfile(params: { : params.credential.type === "token" ? { ...params.credential, token: normalizeSecretInput(params.credential.token) } : params.credential; - const store = ensureAuthProfileStore(params.agentDir); + const store = ensureAuthProfileStoreForLocalUpdate(params.agentDir); store.profiles[params.profileId] = credential; - saveAuthProfileStore(store, params.agentDir); + saveAuthProfileStore(store, params.agentDir, { syncExternalCli: false }); } export async function upsertAuthProfileWithLock(params: { diff --git a/src/agents/auth-profiles/store.ts b/src/agents/auth-profiles/store.ts index 75636d4c144..6a075f7ca95 100644 --- a/src/agents/auth-profiles/store.ts +++ b/src/agents/auth-profiles/store.ts @@ -29,6 +29,11 @@ import type { AuthProfileStore } from "./types.js"; type LoadAuthProfileStoreOptions = { allowKeychainPrompt?: boolean; readOnly?: boolean; + syncExternalCli?: boolean; +}; + +type SaveAuthProfileStoreOptions = { + syncExternalCli?: boolean; }; const runtimeAuthStoreSnapshots = new Map(); @@ -192,6 +197,10 @@ function syncExternalCliCredentialsTimed( return mutated; } +function shouldSyncExternalCliCredentials(options?: { syncExternalCli?: boolean }): boolean { + return options?.syncExternalCli !== false; +} + export function loadAuthProfileStore(): AuthProfileStore { const asStore = loadPersistedAuthProfileStore(); if (asStore) { @@ -238,7 +247,9 @@ function loadAuthProfileStoreForAgent( if (asStore) { // Runtime secret activation must remain read-only: // sync external CLI credentials in-memory, but never persist while readOnly. - syncExternalCliCredentialsTimed(asStore, { log: !readOnly }); + if (shouldSyncExternalCliCredentials(options)) { + syncExternalCliCredentialsTimed(asStore, { log: !readOnly }); + } if (!readOnly) { writeCachedAuthProfileStore({ authPath, @@ -279,7 +290,9 @@ function loadAuthProfileStoreForAgent( const mergedOAuth = mergeOAuthFileIntoStore(store); // Keep external CLI credentials visible in runtime even during read-only loads. - syncExternalCliCredentialsTimed(store, { log: !readOnly }); + if (shouldSyncExternalCliCredentials(options)) { + syncExternalCliCredentialsTimed(store, { log: !readOnly }); + } const forceReadOnly = process.env.OPENCLAW_AUTH_STORE_READONLY === "1"; const shouldWrite = !readOnly && !forceReadOnly && (legacy !== null || mergedOAuth); if (shouldWrite) { @@ -357,6 +370,22 @@ export function ensureAuthProfileStore( return overlayExternalAuthProfiles(merged, { agentDir }); } +export function ensureAuthProfileStoreForLocalUpdate(agentDir?: string): AuthProfileStore { + const options: LoadAuthProfileStoreOptions = { syncExternalCli: false }; + const store = loadAuthProfileStoreForAgent(agentDir, options); + const authPath = resolveAuthStorePath(agentDir); + const mainAuthPath = resolveAuthStorePath(); + if (!agentDir || authPath === mainAuthPath) { + return store; + } + + const mainStore = loadAuthProfileStoreForAgent(undefined, { + readOnly: true, + syncExternalCli: false, + }); + return mergeAuthProfileStores(mainStore, store); +} + export function hasAnyAuthProfileStoreSource(agentDir?: string): boolean { const runtimeStore = resolveRuntimeAuthProfileStore(agentDir); if (runtimeStore && Object.keys(runtimeStore.profiles).length > 0) { @@ -376,7 +405,11 @@ export function hasAnyAuthProfileStoreSource(agentDir?: string): boolean { return false; } -export function saveAuthProfileStore(store: AuthProfileStore, agentDir?: string): void { +export function saveAuthProfileStore( + store: AuthProfileStore, + agentDir?: string, + options?: SaveAuthProfileStoreOptions, +): void { const authPath = resolveAuthStorePath(agentDir); const statePath = resolveAuthStatePath(agentDir); const runtimeKey = resolveRuntimeStoreKey(agentDir); @@ -394,7 +427,9 @@ export function saveAuthProfileStore(store: AuthProfileStore, agentDir?: string) saveJsonFile(authPath, payload); savePersistedAuthProfileState(store, agentDir); const runtimeStore = cloneAuthProfileStore(store); - syncExternalCliCredentialsTimed(runtimeStore, { log: false }); + if (shouldSyncExternalCliCredentials(options)) { + syncExternalCliCredentialsTimed(runtimeStore, { log: false }); + } writeCachedAuthProfileStore({ authPath, authMtimeMs: readAuthStoreMtimeMs(authPath), diff --git a/src/commands/auth-choice.test.ts b/src/commands/auth-choice.test.ts index 9b0a03e0032..348b072fad1 100644 --- a/src/commands/auth-choice.test.ts +++ b/src/commands/auth-choice.test.ts @@ -36,6 +36,7 @@ vi.mock("../plugins/provider-openai-codex-oauth.js", () => ({ })); const resolvePluginProviders = vi.hoisted(() => vi.fn<() => ProviderPlugin[]>(() => [])); +const runProviderModelSelectedHook = vi.hoisted(() => vi.fn(async () => {})); vi.mock("../plugins/provider-auth-choice.runtime.js", async () => { const actual = await vi.importActual( "../plugins/provider-auth-choice.runtime.js", @@ -43,6 +44,7 @@ vi.mock("../plugins/provider-auth-choice.runtime.js", async () => { return { ...actual, resolvePluginProviders, + runProviderModelSelectedHook, }; }); @@ -644,6 +646,7 @@ describe("applyAuthChoice", () => { vi.unstubAllGlobals(); resolvePluginProviders.mockReset(); resolvePluginProviders.mockReturnValue(createDefaultProviderPlugins()); + runProviderModelSelectedHook.mockClear(); detectZaiEndpoint.mockReset(); detectZaiEndpoint.mockResolvedValue(null); loginOpenAICodexOAuth.mockReset();