From 34b17c82dacefac73081152f8c6fa8b695e84a8b Mon Sep 17 00:00:00 2001 From: Shakker Date: Sat, 2 May 2026 04:26:02 +0100 Subject: [PATCH] fix: keep oauth refresh on persisted auth stores --- .../auth-profiles/oauth-manager.test.ts | 89 +++++++++++++++++++ src/agents/auth-profiles/oauth-manager.ts | 20 +++-- 2 files changed, 101 insertions(+), 8 deletions(-) diff --git a/src/agents/auth-profiles/oauth-manager.test.ts b/src/agents/auth-profiles/oauth-manager.test.ts index 259bf9b916d..c906849ddb8 100644 --- a/src/agents/auth-profiles/oauth-manager.test.ts +++ b/src/agents/auth-profiles/oauth-manager.test.ts @@ -14,6 +14,7 @@ import { import { clearRuntimeAuthProfileStoreSnapshots, ensureAuthProfileStore, + ensureAuthProfileStoreWithoutExternalProfiles, saveAuthProfileStore, } from "./store.js"; import type { AuthProfileStore, OAuthCredential } from "./types.js"; @@ -145,6 +146,94 @@ describe("OAuthManagerRefreshError", () => { }); describe("createOAuthManager", () => { + it("does not overlay external auth while checking main-store adoption", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "oauth-manager-main-adopt-")); + tempDirs.push(tempRoot); + process.env.OPENCLAW_STATE_DIR = tempRoot; + const mainAgentDir = path.join(tempRoot, "agents", "main", "agent"); + const agentDir = path.join(tempRoot, "agents", "sub", "agent"); + process.env.OPENCLAW_AGENT_DIR = mainAgentDir; + process.env.PI_CODING_AGENT_DIR = mainAgentDir; + await fs.mkdir(agentDir, { recursive: true }); + await fs.mkdir(mainAgentDir, { recursive: true }); + + const profileId = "openai-codex:default"; + const subCredential = createCredential({ + access: "expired-sub-access", + refresh: "sub-refresh", + expires: Date.now() - 60_000, + }); + const mainCredential = createCredential({ + access: "expired-main-access", + refresh: "main-refresh", + expires: Date.now() - 30_000, + }); + saveAuthProfileStore( + { + version: 1, + profiles: { + [profileId]: subCredential, + }, + }, + agentDir, + { filterExternalAuthProfiles: false }, + ); + saveAuthProfileStore( + { + version: 1, + profiles: { + [profileId]: mainCredential, + }, + }, + mainAgentDir, + { filterExternalAuthProfiles: false }, + ); + externalAuthTesting.setResolveExternalAuthProfilesForTest(() => [ + { + profileId, + credential: createCredential({ + access: "external-fresh-access", + refresh: "external-fresh-refresh", + expires: Date.now() + 60_000, + }), + persistence: "runtime-only", + }, + ]); + + const refreshCredential = vi.fn(async (credential: OAuthCredential) => { + expect(credential.access).toBe("expired-main-access"); + return { + access: "rotated-main-access", + refresh: "rotated-main-refresh", + expires: Date.now() + 60_000, + }; + }); + const manager = createOAuthManager({ + buildApiKey: async (_provider, credential) => credential.access, + refreshCredential, + readBootstrapCredential: () => null, + isRefreshTokenReusedError: () => false, + }); + + const result = await manager.resolveOAuthAccess({ + store: ensureAuthProfileStoreWithoutExternalProfiles(agentDir, { + allowKeychainPrompt: false, + }), + profileId, + credential: subCredential, + agentDir, + }); + + expect(refreshCredential).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + apiKey: "rotated-main-access", + credential: expect.objectContaining({ + access: "rotated-main-access", + refresh: "rotated-main-refresh", + }), + }); + }); + it("refreshes with the adopted external oauth credential", async () => { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "oauth-manager-refresh-")); tempDirs.push(tempRoot); diff --git a/src/agents/auth-profiles/oauth-manager.ts b/src/agents/auth-profiles/oauth-manager.ts index c37f375b426..d426579ecb6 100644 --- a/src/agents/auth-profiles/oauth-manager.ts +++ b/src/agents/auth-profiles/oauth-manager.ts @@ -25,8 +25,8 @@ import { } from "./oauth-shared.js"; import { ensureAuthStoreFile, resolveAuthStorePath, resolveOAuthRefreshLockPath } from "./paths.js"; import { - ensureAuthProfileStore, - loadAuthProfileStoreForSecretsRuntime, + ensureAuthProfileStoreWithoutExternalProfiles, + loadAuthProfileStoreWithoutExternalProfiles, saveAuthProfileStore, resolvePersistedAuthProfileOwnerAgentDir, updateAuthProfileStoreWithLock, @@ -143,7 +143,7 @@ async function loadFreshStoredOAuthCredential(params: { previous?: Pick; requireChange?: boolean; }): Promise { - const reloadedStore = loadAuthProfileStoreForSecretsRuntime(params.agentDir); + const reloadedStore = loadAuthProfileStoreWithoutExternalProfiles(params.agentDir); const reloaded = reloadedStore.profiles[params.profileId]; if ( reloaded?.type !== "oauth" || @@ -217,7 +217,9 @@ export function createOAuthManager(adapter: OAuthManagerAdapter) { return null; } try { - const mainStore = ensureAuthProfileStore(undefined); + const mainStore = ensureAuthProfileStoreWithoutExternalProfiles(undefined, { + allowKeychainPrompt: false, + }); const mainCred = mainStore.profiles[params.profileId]; if ( mainCred?.type === "oauth" && @@ -325,7 +327,7 @@ export function createOAuthManager(adapter: OAuthManagerAdapter) { try { return await withFileLock(globalRefreshLockPath, OAUTH_REFRESH_LOCK_OPTIONS, async () => withFileLock(authPath, AUTH_STORE_LOCK_OPTIONS, async () => { - const store = loadAuthProfileStoreForSecretsRuntime(ownerAgentDir); + const store = loadAuthProfileStoreWithoutExternalProfiles(ownerAgentDir); const cred = store.profiles[params.profileId]; if (!cred || cred.type !== "oauth") { return null; @@ -341,7 +343,7 @@ export function createOAuthManager(adapter: OAuthManagerAdapter) { if (params.agentDir) { try { - const mainStore = loadAuthProfileStoreForSecretsRuntime(undefined); + const mainStore = loadAuthProfileStoreWithoutExternalProfiles(undefined); const mainCred = mainStore.profiles[params.profileId]; if ( mainCred?.type === "oauth" && @@ -517,7 +519,7 @@ export function createOAuthManager(adapter: OAuthManagerAdapter) { }); return refreshed; } catch (error) { - const refreshedStore = loadAuthProfileStoreForSecretsRuntime(params.agentDir); + const refreshedStore = loadAuthProfileStoreWithoutExternalProfiles(params.agentDir); const refreshed = refreshedStore.profiles[params.profileId]; if (refreshed?.type === "oauth" && hasUsableOAuthCredential(refreshed)) { return { @@ -560,7 +562,9 @@ export function createOAuthManager(adapter: OAuthManagerAdapter) { } if (params.agentDir) { try { - const mainStore = ensureAuthProfileStore(undefined); + const mainStore = ensureAuthProfileStoreWithoutExternalProfiles(undefined, { + allowKeychainPrompt: false, + }); const mainCred = mainStore.profiles[params.profileId]; if ( mainCred?.type === "oauth" &&