diff --git a/src/agents/auth-profiles/persisted.ts b/src/agents/auth-profiles/persisted.ts index d38e392f230..8793ff66ac6 100644 --- a/src/agents/auth-profiles/persisted.ts +++ b/src/agents/auth-profiles/persisted.ts @@ -460,7 +460,11 @@ function createFallbackOAuthProfileSecretKeyFile(): string | undefined { } function shouldUseMacKeychainForOAuthProfileSecrets(): boolean { - return process.platform === "darwin" && process.env.VITEST !== "true"; + return ( + process.platform === "darwin" && + process.env.VITEST !== "true" && + process.env.VITEST_WORKER_ID === undefined + ); } function resolveOAuthProfileSecretKeySeed(options?: { create?: boolean }): string | undefined { diff --git a/src/agents/auth-profiles/profiles.test.ts b/src/agents/auth-profiles/profiles.test.ts index 3462d7135d7..284cbc7f319 100644 --- a/src/agents/auth-profiles/profiles.test.ts +++ b/src/agents/auth-profiles/profiles.test.ts @@ -5,7 +5,7 @@ import { describe, expect, it, vi } from "vitest"; import { resolveOAuthDir } from "../../config/paths.js"; import { AUTH_STORE_VERSION } from "./constants.js"; import { resolveAuthStorePath } from "./paths.js"; -import { promoteAuthProfileInOrder } from "./profiles.js"; +import { promoteAuthProfileInOrder, upsertAuthProfileWithLock } from "./profiles.js"; import { clearRuntimeAuthProfileStoreSnapshots, findPersistedAuthProfileCredential, @@ -138,6 +138,54 @@ function expectOpenClawCredentialsOAuthRef( } describe("promoteAuthProfileInOrder", () => { + it("normalizes copied secrets when using the locked upsert path", async () => { + const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-profile-upsert-")); + const agentDir = path.join(stateDir, "agents", "main", "agent"); + const previousStateDir = process.env.OPENCLAW_STATE_DIR; + process.env.OPENCLAW_STATE_DIR = stateDir; + try { + fs.mkdirSync(agentDir, { recursive: true }); + + await upsertAuthProfileWithLock({ + profileId: "openai:manual", + credential: { + type: "token", + provider: "openai", + token: " bearer\r\n-token\u2502 ", + }, + agentDir, + }); + await upsertAuthProfileWithLock({ + profileId: "anthropic:key", + credential: { + type: "api_key", + provider: "anthropic", + key: " sk-\r\nant\u2502 ", + }, + agentDir, + }); + + const profiles = loadAuthProfileStoreWithoutExternalProfiles(agentDir).profiles; + expect(profiles["openai:manual"]).toMatchObject({ + type: "token", + provider: "openai", + token: "bearer-token", + }); + expect(profiles["anthropic:key"]).toMatchObject({ + type: "api_key", + provider: "anthropic", + key: "sk-ant", + }); + } finally { + if (previousStateDir === undefined) { + delete process.env.OPENCLAW_STATE_DIR; + } else { + process.env.OPENCLAW_STATE_DIR = previousStateDir; + } + fs.rmSync(stateDir, { recursive: true, force: true }); + } + }); + it("omits inline openai-codex oauth secrets from persisted auth profile files", () => { const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-profile-metadata-")); const agentDir = path.join(stateDir, "agents", "main", "agent"); @@ -610,10 +658,18 @@ describe("promoteAuthProfileInOrder", () => { { filterExternalAuthProfiles: false }, ); + const expectedKeyPath = + process.platform === "darwin" + ? path.join( + homeDir, + "Library", + "Application Support", + "OpenClaw", + "auth-profile-secret-key", + ) + : path.join(homeDir, ".openclaw-auth-profile-secrets", "auth-profile-secret-key"); const keyPaths = findFilesNamed(rootDir, "auth-profile-secret-key"); - expect(keyPaths).toEqual([ - path.join(homeDir, ".openclaw-auth-profile-secrets", "auth-profile-secret-key"), - ]); + expect(keyPaths).toEqual([expectedKeyPath]); expect(keyPaths.every((keyPath) => !isPathInsideOrEqual(stateDir, keyPath))).toBe(true); const keyValues = keyPaths.map((keyPath) => fs.readFileSync(keyPath, "utf8").trim()); const persistedStateTree = readPersistedTree(stateDir); diff --git a/src/agents/auth-profiles/profiles.ts b/src/agents/auth-profiles/profiles.ts index 62af2a6a597..4d37d977a3b 100644 --- a/src/agents/auth-profiles/profiles.ts +++ b/src/agents/auth-profiles/profiles.ts @@ -106,22 +106,35 @@ export async function promoteAuthProfileInOrder(params: { }); } +function normalizeAuthProfileCredential(credential: AuthProfileCredential): AuthProfileCredential { + if (credential.type === "api_key") { + if (typeof credential.key !== "string") { + return credential; + } + const { key: _key, ...rest } = credential; + const key = normalizeSecretInput(credential.key); + return { + ...rest, + ...(key ? { key } : {}), + }; + } + if (credential.type === "token") { + if (typeof credential.token !== "string") { + return credential; + } + const { token: _token, ...rest } = credential; + const token = normalizeSecretInput(credential.token); + return { ...rest, ...(token ? { token } : {}) }; + } + return credential; +} + export function upsertAuthProfile(params: { profileId: string; credential: AuthProfileCredential; agentDir?: string; }): void { - const credential = - params.credential.type === "api_key" - ? { - ...params.credential, - ...(typeof params.credential.key === "string" - ? { key: normalizeSecretInput(params.credential.key) } - : {}), - } - : params.credential.type === "token" - ? { ...params.credential, token: normalizeSecretInput(params.credential.token) } - : params.credential; + const credential = normalizeAuthProfileCredential(params.credential); const store = ensureAuthProfileStoreForLocalUpdate(params.agentDir); store.profiles[params.profileId] = credential; saveAuthProfileStore(store, params.agentDir, { @@ -135,10 +148,15 @@ export async function upsertAuthProfileWithLock(params: { credential: AuthProfileCredential; agentDir?: string; }): Promise { + const credential = normalizeAuthProfileCredential(params.credential); return await updateAuthProfileStoreWithLock({ agentDir: params.agentDir, + saveOptions: { + filterExternalAuthProfiles: false, + syncExternalCli: false, + }, updater: (store) => { - store.profiles[params.profileId] = params.credential; + store.profiles[params.profileId] = credential; return true; }, }); diff --git a/src/agents/auth-profiles/store.ts b/src/agents/auth-profiles/store.ts index ea88ee202ad..b7779bc3ee3 100644 --- a/src/agents/auth-profiles/store.ts +++ b/src/agents/auth-profiles/store.ts @@ -456,6 +456,7 @@ function buildLocalAuthProfileStoreForSave(params: { export async function updateAuthProfileStoreWithLock(params: { agentDir?: string; + saveOptions?: SaveAuthProfileStoreOptions; updater: (store: AuthProfileStore) => boolean; }): Promise { const authPath = resolveAuthStorePath(params.agentDir); @@ -469,7 +470,7 @@ export async function updateAuthProfileStoreWithLock(params: { const store = loadAuthProfileStoreForAgent(params.agentDir, { syncExternalCli: false }); const shouldSave = params.updater(store); if (shouldSave) { - saveAuthProfileStore(store, params.agentDir); + saveAuthProfileStore(store, params.agentDir, params.saveOptions); } return store; }); diff --git a/src/agents/auth-profiles/upsert-with-lock.ts b/src/agents/auth-profiles/upsert-with-lock.ts index 9bf30db8708..76e5cd5fa32 100644 --- a/src/agents/auth-profiles/upsert-with-lock.ts +++ b/src/agents/auth-profiles/upsert-with-lock.ts @@ -1,7 +1,31 @@ +import { normalizeSecretInput } from "../../utils/normalize-secret-input.js"; import { ensureAuthStoreFile, resolveAuthStorePath } from "./paths.js"; import { updateAuthProfileStoreWithLock } from "./store.js"; import type { AuthProfileCredential, AuthProfileStore } from "./types.js"; +function normalizeAuthProfileCredential(credential: AuthProfileCredential): AuthProfileCredential { + if (credential.type === "api_key") { + if (typeof credential.key !== "string") { + return credential; + } + const { key: _key, ...rest } = credential; + const key = normalizeSecretInput(credential.key); + return { + ...rest, + ...(key ? { key } : {}), + }; + } + if (credential.type === "token") { + if (typeof credential.token !== "string") { + return credential; + } + const { token: _token, ...rest } = credential; + const token = normalizeSecretInput(credential.token); + return { ...rest, ...(token ? { token } : {}) }; + } + return credential; +} + export async function upsertAuthProfileWithLock(params: { profileId: string; credential: AuthProfileCredential; @@ -11,10 +35,15 @@ export async function upsertAuthProfileWithLock(params: { ensureAuthStoreFile(authPath); try { + const credential = normalizeAuthProfileCredential(params.credential); return await updateAuthProfileStoreWithLock({ agentDir: params.agentDir, + saveOptions: { + filterExternalAuthProfiles: false, + syncExternalCli: false, + }, updater: (store) => { - store.profiles[params.profileId] = params.credential; + store.profiles[params.profileId] = credential; return true; }, });