diff --git a/src/agents/auth-profiles.ts b/src/agents/auth-profiles.ts index f8d391a6158..cc2d3acd8d0 100644 --- a/src/agents/auth-profiles.ts +++ b/src/agents/auth-profiles.ts @@ -22,8 +22,9 @@ export { resolveAuthProfileOrder, } from "./auth-profiles/order.js"; export { + resolveAuthProfileStoreAgentDir, + resolveAuthProfileStoreLocationForDisplay, resolveAuthStatePathForDisplay, - resolveAuthStorePathForDisplay, } from "./auth-profiles/paths.js"; export { dedupeProfileIds, diff --git a/src/agents/auth-profiles/constants.ts b/src/agents/auth-profiles/constants.ts index 77e79488814..81e819f5cdd 100644 --- a/src/agents/auth-profiles/constants.ts +++ b/src/agents/auth-profiles/constants.ts @@ -1,5 +1,9 @@ import { createSubsystemLogger } from "../../logging/subsystem.js"; -export { AUTH_PROFILE_FILENAME, AUTH_STATE_FILENAME } from "./path-constants.js"; +export { + AUTH_PROFILE_FILENAME, + AUTH_PROFILE_STORE_KV_SCOPE, + AUTH_STATE_FILENAME, +} from "./path-constants.js"; export const AUTH_STORE_VERSION = 1; diff --git a/src/agents/auth-profiles/oauth-manager.ts b/src/agents/auth-profiles/oauth-manager.ts index 6d0cb009166..3272988caa7 100644 --- a/src/agents/auth-profiles/oauth-manager.ts +++ b/src/agents/auth-profiles/oauth-manager.ts @@ -21,7 +21,7 @@ import { } from "./oauth-shared.js"; import { OAUTH_REFRESH_LOCK_SCOPE, - resolveAuthStorePath, + resolveAuthProfileStoreKey, resolveOAuthRefreshLockKey, } from "./paths.js"; import { @@ -323,7 +323,7 @@ export function createOAuthManager(adapter: OAuthManagerAdapter) { cfg?: OpenClawConfig; }): Promise { const ownerAgentDir = resolvePersistedAuthProfileOwnerAgentDir(params); - const authPath = resolveAuthStorePath(ownerAgentDir); + const ownerStoreKey = resolveAuthProfileStoreKey(ownerAgentDir); const refreshLockKey = resolveOAuthRefreshLockKey(params.provider, params.profileId); try { @@ -452,8 +452,8 @@ export function createOAuthManager(adapter: OAuthManagerAdapter) { store.profiles[params.profileId] = refreshedCredentials; saveAuthProfileStore(store, ownerAgentDir); if (ownerAgentDir) { - const mainPath = resolveAuthStorePath(undefined); - if (mainPath !== authPath) { + const mainStoreKey = resolveAuthProfileStoreKey(undefined); + if (mainStoreKey !== ownerStoreKey) { await mirrorRefreshedCredentialIntoMainStore({ profileId: params.profileId, refreshed: refreshedCredentials, diff --git a/src/agents/auth-profiles/path-constants.ts b/src/agents/auth-profiles/path-constants.ts index 9329df974c1..8fa5bc42b27 100644 --- a/src/agents/auth-profiles/path-constants.ts +++ b/src/agents/auth-profiles/path-constants.ts @@ -1,2 +1,3 @@ export const AUTH_PROFILE_FILENAME = "auth-profiles.json"; export const AUTH_STATE_FILENAME = "auth-state.json"; +export const AUTH_PROFILE_STORE_KV_SCOPE = "auth-profiles"; diff --git a/src/agents/auth-profiles/path-resolve.ts b/src/agents/auth-profiles/path-resolve.ts index 393c0a6d731..9ba8abd5950 100644 --- a/src/agents/auth-profiles/path-resolve.ts +++ b/src/agents/auth-profiles/path-resolve.ts @@ -1,16 +1,36 @@ import { createHash } from "node:crypto"; import path from "node:path"; +import { resolveOpenClawStateSqlitePath } from "../../state/openclaw-state-db.paths.js"; import { resolveUserPath } from "../../utils.js"; import { resolveDefaultAgentDir } from "../agent-scope-config.js"; -import { AUTH_PROFILE_FILENAME, AUTH_STATE_FILENAME } from "./path-constants.js"; +import { + AUTH_PROFILE_FILENAME, + AUTH_PROFILE_STORE_KV_SCOPE, + AUTH_STATE_FILENAME, +} from "./path-constants.js"; + +export function resolveAuthProfileStoreAgentDir(agentDir?: string): string { + return resolveUserPath(agentDir ?? resolveDefaultAgentDir({})); +} + +export function resolveAuthProfileStoreKey(agentDir?: string): string { + return resolveAuthProfileStoreAgentDir(agentDir); +} + +export function resolveAuthProfileStoreLocationForDisplay( + agentDir?: string, + env: NodeJS.ProcessEnv = process.env, +): string { + return `${resolveOpenClawStateSqlitePath(env)}#kv/${AUTH_PROFILE_STORE_KV_SCOPE}/${resolveAuthProfileStoreKey(agentDir)}`; +} export function resolveAuthStorePath(agentDir?: string): string { - const resolved = resolveUserPath(agentDir ?? resolveDefaultAgentDir({})); + const resolved = resolveAuthProfileStoreAgentDir(agentDir); return path.join(resolved, AUTH_PROFILE_FILENAME); } export function resolveAuthStatePath(agentDir?: string): string { - const resolved = resolveUserPath(agentDir ?? resolveDefaultAgentDir({})); + const resolved = resolveAuthProfileStoreAgentDir(agentDir); return path.join(resolved, AUTH_STATE_FILENAME); } diff --git a/src/agents/auth-profiles/paths.ts b/src/agents/auth-profiles/paths.ts index 21132a65141..76f1aa12c37 100644 --- a/src/agents/auth-profiles/paths.ts +++ b/src/agents/auth-profiles/paths.ts @@ -1,4 +1,7 @@ export { + resolveAuthProfileStoreAgentDir, + resolveAuthProfileStoreKey, + resolveAuthProfileStoreLocationForDisplay, resolveAuthStatePath, resolveAuthStatePathForDisplay, resolveAuthStorePath, diff --git a/src/agents/auth-profiles/persisted.ts b/src/agents/auth-profiles/persisted.ts index 1a878d22922..1679b1f164c 100644 --- a/src/agents/auth-profiles/persisted.ts +++ b/src/agents/auth-profiles/persisted.ts @@ -9,7 +9,7 @@ import { type OpenClawStateJsonValue, } from "../../state/openclaw-state-kv.js"; import { normalizeProviderId } from "../provider-id.js"; -import { AUTH_STORE_VERSION, log } from "./constants.js"; +import { AUTH_PROFILE_STORE_KV_SCOPE, AUTH_STORE_VERSION, log } from "./constants.js"; import { hasOAuthIdentity, hasUsableOAuthCredential, @@ -17,7 +17,7 @@ import { normalizeAuthEmailToken, normalizeAuthIdentityToken, } from "./oauth-shared.js"; -import { resolveAuthStorePath } from "./paths.js"; +import { resolveAuthProfileStoreKey } from "./paths.js"; import { coerceAuthProfileState, loadPersistedAuthProfileState, @@ -33,10 +33,10 @@ import type { ProfileUsageStats, } from "./types.js"; -export const AUTH_PROFILE_STORE_KV_SCOPE = "auth-profiles"; +export { AUTH_PROFILE_STORE_KV_SCOPE } from "./constants.js"; export function authProfileStoreKey(agentDir?: string): string { - return resolveAuthStorePath(agentDir); + return resolveAuthProfileStoreKey(agentDir); } type CredentialRejectReason = "non_object" | "invalid_type" | "missing_provider"; diff --git a/src/agents/auth-profiles/runtime-snapshots.ts b/src/agents/auth-profiles/runtime-snapshots.ts index 8c620eb438e..bdd2ac7948a 100644 --- a/src/agents/auth-profiles/runtime-snapshots.ts +++ b/src/agents/auth-profiles/runtime-snapshots.ts @@ -1,11 +1,11 @@ import { cloneAuthProfileStore } from "./clone.js"; -import { resolveAuthStorePath } from "./path-resolve.js"; +import { resolveAuthProfileStoreKey } from "./path-resolve.js"; import type { AuthProfileStore } from "./types.js"; const runtimeAuthStoreSnapshots = new Map(); function resolveRuntimeStoreKey(agentDir?: string): string { - return resolveAuthStorePath(agentDir); + return resolveAuthProfileStoreKey(agentDir); } export function getRuntimeAuthProfileStoreSnapshot( diff --git a/src/agents/auth-profiles/source-check.ts b/src/agents/auth-profiles/source-check.ts index a246f85c765..4bc4a85d446 100644 --- a/src/agents/auth-profiles/source-check.ts +++ b/src/agents/auth-profiles/source-check.ts @@ -1,4 +1,4 @@ -import { resolveAuthStorePath } from "./path-resolve.js"; +import { resolveAuthProfileStoreKey } from "./path-resolve.js"; import { hasPersistedAuthProfileSecretsStore } from "./persisted.js"; import { hasAnyRuntimeAuthProfileStoreSource } from "./runtime-snapshots.js"; @@ -10,9 +10,9 @@ export function hasAnyAuthProfileStoreSource(agentDir?: string): boolean { return true; } - const authPath = resolveAuthStorePath(agentDir); - const mainAuthPath = resolveAuthStorePath(); - if (agentDir && authPath !== mainAuthPath && hasPersistedAuthProfileSecretsStore(undefined)) { + const storeKey = resolveAuthProfileStoreKey(agentDir); + const mainStoreKey = resolveAuthProfileStoreKey(); + if (agentDir && storeKey !== mainStoreKey && hasPersistedAuthProfileSecretsStore(undefined)) { return true; } return false; diff --git a/src/agents/auth-profiles/store.ts b/src/agents/auth-profiles/store.ts index 70447603ce0..ca6f7d238d3 100644 --- a/src/agents/auth-profiles/store.ts +++ b/src/agents/auth-profiles/store.ts @@ -10,7 +10,7 @@ import { AUTH_STORE_VERSION, EXTERNAL_CLI_SYNC_TTL_MS } from "./constants.js"; import { overlayExternalAuthProfiles, shouldPersistExternalAuthProfile } from "./external-auth.js"; import type { ExternalCliAuthDiscovery } from "./external-cli-discovery.js"; import { isSafeToAdoptMainStoreOAuthIdentity } from "./oauth-shared.js"; -import { resolveAuthStorePath } from "./paths.js"; +import { resolveAuthProfileStoreKey } from "./paths.js"; import { buildPersistedAuthProfileSecretsStore, loadPersistedAuthProfileStoreEntry, @@ -70,9 +70,9 @@ function isInheritedMainOAuthCredential(params: { if (!params.agentDir || params.credential.type !== "oauth") { return false; } - const authPath = resolveAuthStorePath(params.agentDir); - const mainAuthPath = resolveAuthStorePath(); - if (authPath === mainAuthPath) { + const storeKey = resolveAuthProfileStoreKey(params.agentDir); + const mainStoreKey = resolveAuthProfileStoreKey(); + if (storeKey === mainStoreKey) { return false; } @@ -112,8 +112,8 @@ function shouldUseMainOwnerForLocalOAuthCredential(params: { } function resolveRuntimeAuthProfileStore(agentDir?: string): AuthProfileStore | null { - const mainKey = resolveAuthStorePath(undefined); - const requestedKey = resolveAuthStorePath(agentDir); + const mainKey = resolveAuthProfileStoreKey(undefined); + const requestedKey = resolveAuthProfileStoreKey(agentDir); const mainStore = getRuntimeAuthProfileStoreSnapshot(undefined); const requestedStore = getRuntimeAuthProfileStoreSnapshot(agentDir); @@ -142,10 +142,10 @@ function resolveRuntimeAuthProfileStore(agentDir?: string): AuthProfileStore | n } function readCachedAuthProfileStore(params: { - authPath: string; + storeKey: string; authMtimeMs: number | null; }): AuthProfileStore | null { - const cached = loadedAuthStoreCache.get(params.authPath); + const cached = loadedAuthStoreCache.get(params.storeKey); if (!cached || cached.authMtimeMs !== params.authMtimeMs) { return null; } @@ -156,11 +156,11 @@ function readCachedAuthProfileStore(params: { } function writeCachedAuthProfileStore(params: { - authPath: string; + storeKey: string; authMtimeMs: number | null; store: AuthProfileStore; }): void { - loadedAuthStoreCache.set(params.authPath, { + loadedAuthStoreCache.set(params.storeKey, { authMtimeMs: params.authMtimeMs, syncedAtMs: Date.now(), store: cloneAuthProfileStore(params.store), @@ -334,12 +334,12 @@ function loadAuthProfileStoreForAgent( options?: LoadAuthProfileStoreOptions, ): AuthProfileStore { const readOnly = options?.readOnly === true; - const authPath = resolveAuthStorePath(agentDir); + const storeKey = resolveAuthProfileStoreKey(agentDir); const persisted = loadPersistedAuthProfileStoreEntry(agentDir, { env: options?.env }); const authMtimeMs = persisted?.updatedAt ?? null; if (!readOnly) { const cached = readCachedAuthProfileStore({ - authPath, + storeKey, authMtimeMs, }); if (cached) { @@ -350,7 +350,7 @@ function loadAuthProfileStoreForAgent( if (asStore) { if (!readOnly) { writeCachedAuthProfileStore({ - authPath, + storeKey, authMtimeMs, store: asStore, }); @@ -365,7 +365,7 @@ function loadAuthProfileStoreForAgent( if (!readOnly) { writeCachedAuthProfileStore({ - authPath, + storeKey, authMtimeMs, store, }); @@ -378,10 +378,10 @@ export function loadAuthProfileStoreForRuntime( options?: LoadAuthProfileStoreOptions, ): AuthProfileStore { const store = loadAuthProfileStoreForAgent(agentDir, options); - const authPath = resolveAuthStorePath(agentDir); - const mainAuthPath = resolveAuthStorePath(); + const storeKey = resolveAuthProfileStoreKey(agentDir); + const mainStoreKey = resolveAuthProfileStoreKey(); const externalCli = resolveExternalCliOverlayOptions(options); - if (!agentDir || authPath === mainAuthPath) { + if (!agentDir || storeKey === mainStoreKey) { return overlayExternalAuthProfiles(store, { agentDir, ...externalCli, @@ -409,9 +409,9 @@ export function loadAuthProfileStoreWithoutExternalProfiles( ...(options?.env ? { env: options.env } : {}), }; const store = loadAuthProfileStoreForAgent(agentDir, loadOptions); - const authPath = resolveAuthStorePath(agentDir); - const mainAuthPath = resolveAuthStorePath(); - if (!agentDir || authPath === mainAuthPath) { + const storeKey = resolveAuthProfileStoreKey(agentDir); + const mainStoreKey = resolveAuthProfileStoreKey(); + if (!agentDir || storeKey === mainStoreKey) { return store; } @@ -448,9 +448,9 @@ export function ensureAuthProfileStoreWithoutExternalProfiles( return runtimeStore; } const store = loadAuthProfileStoreForAgent(agentDir, options); - const authPath = resolveAuthStorePath(agentDir); - const mainAuthPath = resolveAuthStorePath(); - if (!agentDir || authPath === mainAuthPath) { + const storeKey = resolveAuthProfileStoreKey(agentDir); + const mainStoreKey = resolveAuthProfileStoreKey(); + if (!agentDir || storeKey === mainStoreKey) { return store; } @@ -468,9 +468,9 @@ export function findPersistedAuthProfileCredential(params: { return requestedProfile; } - const requestedPath = resolveAuthStorePath(params.agentDir); - const mainPath = resolveAuthStorePath(); - if (requestedPath === mainPath) { + const requestedKey = resolveAuthProfileStoreKey(params.agentDir); + const mainKey = resolveAuthProfileStoreKey(); + if (requestedKey === mainKey) { return requestedProfile; } @@ -485,9 +485,9 @@ export function resolvePersistedAuthProfileOwnerAgentDir(params: { return undefined; } const requestedStore = loadPersistedAuthProfileStore(params.agentDir); - const requestedPath = resolveAuthStorePath(params.agentDir); - const mainPath = resolveAuthStorePath(); - if (requestedPath === mainPath) { + const requestedKey = resolveAuthProfileStoreKey(params.agentDir); + const mainKey = resolveAuthProfileStoreKey(); + if (requestedKey === mainKey) { return undefined; } @@ -508,9 +508,9 @@ export function resolvePersistedAuthProfileOwnerAgentDir(params: { 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) { + const storeKey = resolveAuthProfileStoreKey(agentDir); + const mainStoreKey = resolveAuthProfileStoreKey(); + if (!agentDir || storeKey === mainStoreKey) { return store; } @@ -565,10 +565,10 @@ function refreshAuthProfileStoreCache( agentDir?: string, options: OpenClawStateDatabaseOptions = {}, ): void { - const authPath = resolveAuthStorePath(agentDir); + const storeKey = resolveAuthProfileStoreKey(agentDir); const persisted = loadPersistedAuthProfileStoreEntry(agentDir, options); writeCachedAuthProfileStore({ - authPath, + storeKey, authMtimeMs: persisted?.updatedAt ?? null, store, }); diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index bec09a26c72..6c4a8bbf9f1 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -1,4 +1,3 @@ -import path from "node:path"; import { formatCliCommand } from "../cli/command-format.js"; import { getRuntimeConfigSnapshot } from "../config/config.js"; import type { ModelProviderAuthMode, ModelProviderConfig } from "../config/types.js"; @@ -27,7 +26,8 @@ import { listProfilesForProvider, resolveApiKeyForProfile, resolveAuthProfileOrder, - resolveAuthStorePathForDisplay, + resolveAuthProfileStoreAgentDir, + resolveAuthProfileStoreLocationForDisplay, } from "./auth-profiles.js"; import * as cliCredentials from "./cli-credentials.js"; import { resolveEnvApiKey, type EnvApiKeyResult } from "./model-auth-env.js"; @@ -782,12 +782,12 @@ export async function resolveApiKeyForProvider(params: { } } - const authStorePath = resolveAuthStorePathForDisplay(params.agentDir); - const resolvedAgentDir = path.dirname(authStorePath); + const authStoreLocation = resolveAuthProfileStoreLocationForDisplay(params.agentDir); + const resolvedAgentDir = resolveAuthProfileStoreAgentDir(params.agentDir); throw new Error( [ `No API key found for provider "${provider}".`, - `Auth store: ${authStorePath} (agentDir: ${resolvedAgentDir}).`, + `Auth store: ${authStoreLocation} (agentDir: ${resolvedAgentDir}).`, `Configure auth for this agent (${formatCliCommand("openclaw agents add ")}) or copy only portable static auth profiles from the main agentDir.`, ].join(" "), ); diff --git a/src/auto-reply/reply/directive-handling.auth.test.ts b/src/auto-reply/reply/directive-handling.auth.test.ts index 4281b924775..655e03b9908 100644 --- a/src/auto-reply/reply/directive-handling.auth.test.ts +++ b/src/auto-reply/reply/directive-handling.auth.test.ts @@ -41,7 +41,7 @@ vi.mock("../../agents/auth-profiles.js", () => ({ }, isProfileInCooldown: () => false, resolveAuthProfileDisplayLabel: ({ profileId }: { profileId: string }) => profileId, - resolveAuthStorePathForDisplay: () => "/tmp/auth-profiles.json", + resolveAuthProfileStoreLocationForDisplay: () => "/tmp/openclaw.sqlite#kv/auth-profiles/main", })); vi.mock("../../agents/model-selection.js", () => ({ diff --git a/src/auto-reply/reply/directive-handling.auth.ts b/src/auto-reply/reply/directive-handling.auth.ts index 047b7cc99a8..01244c1361b 100644 --- a/src/auto-reply/reply/directive-handling.auth.ts +++ b/src/auto-reply/reply/directive-handling.auth.ts @@ -3,7 +3,7 @@ import { isConfiguredAwsSdkAuthProfileForProvider, isProfileInCooldown, resolveAuthProfileDisplayLabel, - resolveAuthStorePathForDisplay, + resolveAuthProfileStoreLocationForDisplay, } from "../../agents/auth-profiles.js"; import { ensureAuthProfileStore, @@ -200,7 +200,7 @@ export const resolveAuthLabel = async ( }); return { label: labels.join(", "), - source: `auth-profiles.json: ${formatPath(resolveAuthStorePathForDisplay(agentDir))}`, + source: `SQLite auth store: ${formatPath(resolveAuthProfileStoreLocationForDisplay(agentDir))}`, }; } diff --git a/src/auto-reply/reply/directive-handling.model.test.ts b/src/auto-reply/reply/directive-handling.model.test.ts index b4c5340e867..c683493792a 100644 --- a/src/auto-reply/reply/directive-handling.model.test.ts +++ b/src/auto-reply/reply/directive-handling.model.test.ts @@ -36,7 +36,7 @@ vi.mock("../../agents/auth-profiles.js", () => ({ }, resolveAuthProfileDisplayLabel: ({ profileId }: { profileId: string }) => profileId, resolveAuthProfileOrder: () => [], - resolveAuthStorePathForDisplay: () => "/tmp/auth-profiles.json", + resolveAuthProfileStoreLocationForDisplay: () => "/tmp/openclaw.sqlite#kv/auth-profiles/main", })); vi.mock("../../agents/auth-profiles/store.js", () => { diff --git a/src/auto-reply/reply/directive-handling.model.ts b/src/auto-reply/reply/directive-handling.model.ts index c637f72b3ea..935fae42c04 100644 --- a/src/auto-reply/reply/directive-handling.model.ts +++ b/src/auto-reply/reply/directive-handling.model.ts @@ -1,4 +1,4 @@ -import { resolveAuthStorePathForDisplay } from "../../agents/auth-profiles.js"; +import { resolveAuthProfileStoreLocationForDisplay } from "../../agents/auth-profiles.js"; import { resolveAgentHarnessPolicy } from "../../agents/harness/selection.js"; import { type ModelAliasIndex, @@ -438,7 +438,7 @@ export async function maybeHandleModelDirectiveInfo(params: { modelRefs.activeDiffers ? `Active: ${modelRefs.active.label} (runtime)` : null, `Default: ${defaultLabel}`, `Agent: ${params.activeAgentId}`, - `Auth file: ${formatPath(resolveAuthStorePathForDisplay(params.agentDir))}`, + `Auth store: ${formatPath(resolveAuthProfileStoreLocationForDisplay(params.agentDir))}`, ].filter((line): line is string => Boolean(line)); if (params.resetModelOverride) { lines.push(`(previous selection reset to default)`); diff --git a/src/cli/models-cli.ts b/src/cli/models-cli.ts index 46830caf6cd..761e84c9726 100644 --- a/src/cli/models-cli.ts +++ b/src/cli/models-cli.ts @@ -381,7 +381,7 @@ export function registerModelsCli(program: Command) { auth .command("paste-token") - .description("Paste a token into auth-profiles.json and update config") + .description("Paste a token into the SQLite auth store and update config") .requiredOption("--provider ", "Provider id (e.g. anthropic)") .option("--profile-id ", "Auth profile id (default: :manual)") .option( diff --git a/src/commands/agents.commands.add.ts b/src/commands/agents.commands.add.ts index 9c7f38acc4b..412b6a6f3a4 100644 --- a/src/commands/agents.commands.add.ts +++ b/src/commands/agents.commands.add.ts @@ -1,5 +1,4 @@ import fs from "node:fs/promises"; -import path from "node:path"; import { resolveAgentDir, resolveAgentWorkspaceDir, @@ -9,7 +8,7 @@ import { buildPortableAuthProfileSecretsStoreForAgentCopy, ensureAuthProfileStore, } from "../agents/auth-profiles.js"; -import { resolveAuthStorePath } from "../agents/auth-profiles/paths.js"; +import { resolveAuthProfileStoreKey } from "../agents/auth-profiles/paths.js"; import { hasPersistedAuthProfileSecretsStore, loadPersistedAuthProfileStore, @@ -280,14 +279,14 @@ export async function agentsAddCommand( const defaultAgentId = resolveDefaultAgentId(cfg); if (defaultAgentId !== agentId) { const sourceAgentDir = resolveAgentDir(cfg, defaultAgentId); - const sourceAuthPath = resolveAuthStorePath(sourceAgentDir); - const mainAuthPath = resolveAuthStorePath(undefined); + const sourceAuthStoreKey = resolveAuthProfileStoreKey(sourceAgentDir); + const mainAuthStoreKey = resolveAuthProfileStoreKey(undefined); const sameAuthPath = - normalizeLowercaseStringOrEmpty(path.resolve(sourceAuthPath)) === - normalizeLowercaseStringOrEmpty(path.resolve(resolveAuthStorePath(agentDir))); + normalizeLowercaseStringOrEmpty(sourceAuthStoreKey) === + normalizeLowercaseStringOrEmpty(resolveAuthProfileStoreKey(agentDir)); const sourceIsInheritedMain = - normalizeLowercaseStringOrEmpty(path.resolve(sourceAuthPath)) === - normalizeLowercaseStringOrEmpty(path.resolve(mainAuthPath)); + normalizeLowercaseStringOrEmpty(sourceAuthStoreKey) === + normalizeLowercaseStringOrEmpty(mainAuthStoreKey); if ( !sameAuthPath && hasPersistedAuthProfileSecretsStore(sourceAgentDir) && diff --git a/src/commands/models/list.auth-overview.test.ts b/src/commands/models/list.auth-overview.test.ts index c4ef22ea75a..b4aa77ad48c 100644 --- a/src/commands/models/list.auth-overview.test.ts +++ b/src/commands/models/list.auth-overview.test.ts @@ -16,8 +16,10 @@ vi.mock("../../agents/auth-profiles/persisted.js", () => ({ })); vi.mock("../../agents/auth-profiles/paths.js", () => ({ - resolveAuthStorePathForDisplay: vi.fn((agentDir?: string) => - agentDir ? `${agentDir}/auth-profiles.json` : "/tmp/auth-profiles.json", + resolveAuthProfileStoreLocationForDisplay: vi.fn((agentDir?: string) => + agentDir + ? `/tmp/openclaw.sqlite#kv/auth-profiles/${agentDir}` + : "/tmp/openclaw.sqlite#kv/auth-profiles/main", ), })); @@ -140,7 +142,7 @@ describe("resolveProviderAuthOverview", () => { expect(overview.effective).toEqual({ kind: "profiles", - detail: "/tmp/openclaw-agent-custom/auth-profiles.json", + detail: "/tmp/openclaw.sqlite#kv/auth-profiles//tmp/openclaw-agent-custom", }); }); @@ -170,7 +172,7 @@ describe("resolveProviderAuthOverview", () => { expect(overview.effective).toEqual({ kind: "profiles", - detail: "/tmp/auth-profiles.json", + detail: "/tmp/openclaw.sqlite#kv/auth-profiles/main", }); }); diff --git a/src/commands/models/list.auth-overview.ts b/src/commands/models/list.auth-overview.ts index 4c043795576..62a6ee1a0b8 100644 --- a/src/commands/models/list.auth-overview.ts +++ b/src/commands/models/list.auth-overview.ts @@ -1,6 +1,6 @@ import { formatRemainingShort } from "../../agents/auth-health.js"; import { resolveAuthProfileDisplayLabel } from "../../agents/auth-profiles/display.js"; -import { resolveAuthStorePathForDisplay } from "../../agents/auth-profiles/paths.js"; +import { resolveAuthProfileStoreLocationForDisplay } from "../../agents/auth-profiles/paths.js"; import { loadPersistedAuthProfileStore } from "../../agents/auth-profiles/persisted.js"; import { listProfilesForProvider } from "../../agents/auth-profiles/profiles.js"; import type { AuthProfileStore } from "../../agents/auth-profiles/types.js"; @@ -135,7 +135,7 @@ export function resolveProviderAuthOverview(params: { return { kind: "profiles", detail: shortenHomePath( - resolveAuthStorePathForDisplay( + resolveAuthProfileStoreLocationForDisplay( resolveProfileSourceAgentDir({ agentDir: params.agentDir, profileIds: profiles, diff --git a/src/commands/models/list.status-command.ts b/src/commands/models/list.status-command.ts index a8b5cb76f01..c4116e80ad0 100644 --- a/src/commands/models/list.status-command.ts +++ b/src/commands/models/list.status-command.ts @@ -11,7 +11,7 @@ import { formatRemainingShort, } from "../../agents/auth-health.js"; import { resolveAuthProfileOrder } from "../../agents/auth-profiles/order.js"; -import { resolveAuthStorePathForDisplay } from "../../agents/auth-profiles/paths.js"; +import { resolveAuthProfileStoreLocationForDisplay } from "../../agents/auth-profiles/paths.js"; import { ensureAuthProfileStoreWithoutExternalProfiles as ensureAuthProfileStore } from "../../agents/auth-profiles/store.js"; import type { AuthProfileCredential } from "../../agents/auth-profiles/types.js"; import { resolveProfileUnusableUntilForDisplay } from "../../agents/auth-profiles/usage.js"; @@ -618,7 +618,7 @@ export async function modelsStatusCommand( aliases, allowed, auth: { - storePath: resolveAuthStorePathForDisplay(agentDir), + storePath: resolveAuthProfileStoreLocationForDisplay(agentDir), shellEnvFallback: { enabled: shellFallbackEnabled, appliedKeys: applied, @@ -730,7 +730,7 @@ export async function modelsStatusCommand( `${label("Auth store")}${colorize(rich, theme.muted, ":")} ${colorize( rich, theme.info, - shortenHomePath(resolveAuthStorePathForDisplay(agentDir)), + shortenHomePath(resolveAuthProfileStoreLocationForDisplay(agentDir)), )}`, ); runtime.log( diff --git a/src/commands/models/list.status.test.ts b/src/commands/models/list.status.test.ts index 1a8db2034cd..333ea390b30 100644 --- a/src/commands/models/list.status.test.ts +++ b/src/commands/models/list.status.test.ts @@ -51,8 +51,9 @@ const mocks = vi.hoisted(() => { }), loadPersistedAuthProfileStore: vi.fn().mockReturnValue(store), resolveAuthProfileDisplayLabel: vi.fn(({ profileId }: { profileId: string }) => profileId), - resolveAuthStorePathForDisplay: vi.fn( - (agentDir?: string) => `${agentDir ?? "/tmp/openclaw-agent"}/auth-profiles.json`, + resolveAuthProfileStoreLocationForDisplay: vi.fn( + (agentDir?: string) => + `/tmp/openclaw.sqlite#kv/auth-profiles/${agentDir ?? "/tmp/openclaw-agent"}`, ), resolveProfileUnusableUntilForDisplay: vi.fn().mockReturnValue(undefined), resolveEnvApiKey: vi.fn((provider: string) => { @@ -155,7 +156,7 @@ vi.mock("../../agents/auth-profiles/display.js", () => ({ resolveAuthProfileDisplayLabel: mocks.resolveAuthProfileDisplayLabel, })); vi.mock("../../agents/auth-profiles/paths.js", () => ({ - resolveAuthStorePathForDisplay: mocks.resolveAuthStorePathForDisplay, + resolveAuthProfileStoreLocationForDisplay: mocks.resolveAuthProfileStoreLocationForDisplay, })); vi.mock("../../agents/auth-profiles/persisted.js", () => ({ loadPersistedAuthProfileStore: mocks.loadPersistedAuthProfileStore, @@ -368,7 +369,9 @@ describe("modelsStatusCommand auth overview", () => { expect(mocks.ensureAuthProfileStore).toHaveBeenCalled(); expect(payload.defaultModel).toBe("anthropic/claude-opus-4-6"); expect(payload.configPath).toBe("/tmp/openclaw-dev/openclaw.json"); - expect(payload.auth.storePath).toBe("/tmp/openclaw-agent/auth-profiles.json"); + expect(payload.auth.storePath).toBe( + "/tmp/openclaw.sqlite#kv/auth-profiles//tmp/openclaw-agent", + ); expect(payload.auth.shellEnvFallback.enabled).toBe(true); expect(payload.auth.shellEnvFallback.appliedKeys).toContain("OPENAI_API_KEY"); expect(payload.auth.missingProvidersInUse).toStrictEqual([]); @@ -431,7 +434,9 @@ describe("modelsStatusCommand auth overview", () => { expect(mocks.ensureAuthProfileStore).toHaveBeenCalledWith("/tmp/openclaw-isolated-agent"); const payload = JSON.parse(String((localRuntime.log as Mock).mock.calls[0]?.[0])); expect(payload.agentDir).toBe("/tmp/openclaw-isolated-agent"); - expect(payload.auth.storePath).toBe("/tmp/openclaw-isolated-agent/auth-profiles.json"); + expect(payload.auth.storePath).toBe( + "/tmp/openclaw.sqlite#kv/auth-profiles//tmp/openclaw-isolated-agent", + ); }); it("uses agent overrides and reports sources", async () => { @@ -462,7 +467,7 @@ describe("modelsStatusCommand auth overview", () => { ).find((provider) => provider.provider === "openai-codex"); expect(openAiCodex?.effective).toEqual({ kind: "profiles", - detail: "/tmp/openclaw-agent-custom/auth-profiles.json", + detail: "/tmp/openclaw.sqlite#kv/auth-profiles//tmp/openclaw-agent-custom", }); }, ); diff --git a/src/plugin-sdk/agent-runtime.ts b/src/plugin-sdk/agent-runtime.ts index b8c975ca1c9..0de762e0a8d 100644 --- a/src/plugin-sdk/agent-runtime.ts +++ b/src/plugin-sdk/agent-runtime.ts @@ -66,7 +66,7 @@ export { formatAuthDoctorHint, resolveAuthProfileEligibility, resolveAuthProfileOrder, - resolveAuthStorePathForDisplay, + resolveAuthProfileStoreLocationForDisplay, } from "../agents/auth-profiles.js"; export type { ApiKeyCredential, diff --git a/src/secrets/audit.test.ts b/src/secrets/audit.test.ts index e326b4735d6..8b71e1b4982 100644 --- a/src/secrets/audit.test.ts +++ b/src/secrets/audit.test.ts @@ -2,6 +2,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { savePersistedAuthProfileSecretsStore } from "../agents/auth-profiles/persisted.js"; import { writeStoredModelsConfigRaw } from "../agents/models-config-store.js"; import { runSecretsAudit } from "./audit.js"; @@ -34,6 +35,17 @@ async function writeJsonFile(filePath: string, value: unknown): Promise { await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8"); } +function writeAuthProfileStore(fixture: AuditFixture, profiles: Record): void { + savePersistedAuthProfileSecretsStore( + { + version: 1, + profiles, + }, + fixture.agentDir, + { env: fixture.env }, + ); +} + async function writeExecResolverShellScript(params: { scriptPath: string; logPath: string; @@ -179,10 +191,7 @@ async function seedAuditFixture(fixture: AuditFixture): Promise { await writeJsonFile(fixture.configPath, { models: { providers: seededProvider }, }); - await writeJsonFile(fixture.authStorePath, { - version: 1, - profiles: Object.fromEntries(seededProfiles), - }); + writeAuthProfileStore(fixture, Object.fromEntries(seededProfiles)); writeStoredModelsConfigRaw( fixture.agentDir, `${JSON.stringify({ @@ -280,11 +289,9 @@ describe("secrets audit", () => { }); it("reports malformed sidecar JSON as findings instead of crashing", async () => { - await fs.writeFile(fixture.authStorePath, "{invalid-json", "utf8"); await fs.writeFile(fixture.authJsonPath, "{invalid-json", "utf8"); const report = await runSecretsAudit({ env: fixture.env }); - expectFindingFile(report, fixture.authStorePath); expectFindingFile(report, fixture.authJsonPath); expectFindingCode(report, "REF_UNRESOLVED"); }); @@ -611,10 +618,7 @@ describe("secrets audit", () => { }, }, }); - await writeJsonFile(fixture.authStorePath, { - version: 1, - profiles: {}, - }); + writeAuthProfileStore(fixture, {}); await fs.writeFile(fixture.envPath, "", "utf8"); const report = await runSecretsAudit({ env: fixture.env }); diff --git a/src/secrets/audit.ts b/src/secrets/audit.ts index c1c870cd505..7b291cfc9c4 100644 --- a/src/secrets/audit.ts +++ b/src/secrets/audit.ts @@ -1,6 +1,8 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { resolveAuthProfileStoreLocationForDisplay } from "../agents/auth-profiles/paths.js"; +import { loadPersistedAuthProfileStore } from "../agents/auth-profiles/persisted.js"; import { isNonSecretApiKeyMarker, isSecretRefHeaderValueMarker, @@ -32,7 +34,7 @@ import { import { isNonEmptyString, isRecord } from "./shared.js"; import { listAgentModelCatalogDirs, - listAuthProfileStorePaths, + listAuthProfileStoreAgentDirs, listLegacyAuthJsonPaths, parseEnvAssignmentValue, readJsonObjectIfExists, @@ -262,30 +264,18 @@ function collectConfigSecrets(params: { } function collectAuthStoreSecrets(params: { - authStorePath: string; + agentDir?: string; collector: AuditCollector; defaults?: SecretDefaults; + env?: NodeJS.ProcessEnv; }): void { - if (!fs.existsSync(params.authStorePath)) { + const authStoreLocation = resolveAuthProfileStoreLocationForDisplay(params.agentDir, params.env); + const store = loadPersistedAuthProfileStore(params.agentDir, { env: params.env }); + if (!store || !isRecord(store.profiles)) { return; } - params.collector.filesScanned.add(params.authStorePath); - const parsedResult = readJsonObjectIfExists(params.authStorePath); - if (parsedResult.error) { - addFinding(params.collector, { - code: "REF_UNRESOLVED", - severity: "error", - file: params.authStorePath, - jsonPath: "", - message: `Invalid JSON in auth-profiles store: ${parsedResult.error}`, - }); - return; - } - const parsed = parsedResult.value; - if (!parsed || !isRecord(parsed.profiles)) { - return; - } - for (const entry of iterateAuthProfileCredentials(parsed.profiles)) { + params.collector.filesScanned.add(authStoreLocation); + for (const entry of iterateAuthProfileCredentials(store.profiles)) { if (entry.kind === "api_key" || entry.kind === "token") { const { ref } = resolveSecretInputRef({ value: entry.value, @@ -294,7 +284,7 @@ function collectAuthStoreSecrets(params: { }); if (ref) { params.collector.refAssignments.push({ - file: params.authStorePath, + file: authStoreLocation, path: `profiles.${entry.profileId}.${entry.valueField}`, ref, expected: "string", @@ -306,7 +296,7 @@ function collectAuthStoreSecrets(params: { addFinding(params.collector, { code: "PLAINTEXT_FOUND", severity: "warn", - file: params.authStorePath, + file: authStoreLocation, jsonPath: `profiles.${entry.profileId}.${entry.valueField}`, message: entry.kind === "api_key" @@ -323,7 +313,7 @@ function collectAuthStoreSecrets(params: { addFinding(params.collector, { code: "LEGACY_RESIDUE", severity: "info", - file: params.authStorePath, + file: authStoreLocation, jsonPath: `profiles.${entry.profileId}`, message: "OAuth credentials are present (out of scope for static SecretRef migration).", provider: entry.provider, @@ -690,11 +680,12 @@ export async function runSecretsAudit( configPath, collector, }); - for (const authStorePath of listAuthProfileStorePaths(config, stateDir)) { + for (const agentDir of listAuthProfileStoreAgentDirs(config, stateDir)) { collectAuthStoreSecrets({ - authStorePath, + agentDir, collector, defaults, + env, }); } for (const agentDir of listAgentModelCatalogDirs(config, stateDir, env)) { diff --git a/src/secrets/auth-store-paths.ts b/src/secrets/auth-store-paths.ts index 8991e5ff86d..09ad6d0f6f6 100644 --- a/src/secrets/auth-store-paths.ts +++ b/src/secrets/auth-store-paths.ts @@ -1,7 +1,6 @@ import fs from "node:fs"; import path from "node:path"; import { listAgentIds, resolveAgentDir } from "../agents/agent-scope.js"; -import { resolveAuthStorePath } from "../agents/auth-profiles/paths.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { resolveUserPath } from "../utils.js"; @@ -31,11 +30,3 @@ export function listAuthProfileStoreAgentDirs(config: OpenClawConfig, stateDir: return [...dirs]; } - -export function listAuthProfileStorePaths(config: OpenClawConfig, stateDir: string): string[] { - const paths = new Set(); - for (const agentDir of listAuthProfileStoreAgentDirs(config, stateDir)) { - paths.add(resolveUserPath(resolveAuthStorePath(agentDir))); - } - return [...paths]; -} diff --git a/src/secrets/storage-scan.ts b/src/secrets/storage-scan.ts index 7a5b5f2cd78..187e0219366 100644 --- a/src/secrets/storage-scan.ts +++ b/src/secrets/storage-scan.ts @@ -4,7 +4,7 @@ import { listAgentIds, resolveAgentDir } from "../agents/agent-scope.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { formatErrorMessage } from "../infra/errors.js"; import { resolveUserPath } from "../utils.js"; -import { listAuthProfileStorePaths as listAuthProfileStorePathsFromAuthStorePaths } from "./auth-store-paths.js"; +import { listAuthProfileStoreAgentDirs as listAuthProfileStoreAgentDirsFromAuthStorePaths } from "./auth-store-paths.js"; import { parseEnvValue } from "./shared.js"; function isJsonObject(value: unknown): value is Record { @@ -15,8 +15,8 @@ export function parseEnvAssignmentValue(raw: string): string { return parseEnvValue(raw); } -export function listAuthProfileStorePaths(config: OpenClawConfig, stateDir: string): string[] { - return listAuthProfileStorePathsFromAuthStorePaths(config, stateDir); +export function listAuthProfileStoreAgentDirs(config: OpenClawConfig, stateDir: string): string[] { + return listAuthProfileStoreAgentDirsFromAuthStorePaths(config, stateDir); } export function listLegacyAuthJsonPaths(stateDir: string): string[] { diff --git a/src/security/audit-extra.async.ts b/src/security/audit-extra.async.ts index 2aa1a1f7052..8c07ae72b99 100644 --- a/src/security/audit-extra.async.ts +++ b/src/security/audit-extra.async.ts @@ -649,45 +649,6 @@ export async function collectStateDeepFilesystemFindings(params: { } for (const agentId of ids) { - const agentDir = path.join(params.stateDir, "agents", agentId, "agent"); - const authPath = path.join(agentDir, "auth-profiles.json"); - const authPerms = await inspectPathPermissions(authPath, { - env: params.env, - platform: params.platform, - exec: params.execIcacls, - }); - if (authPerms.ok) { - if (authPerms.worldWritable || authPerms.groupWritable) { - findings.push({ - checkId: "fs.auth_profiles.perms_writable", - severity: "critical", - title: "auth-profiles.json is writable by others", - detail: `${formatPermissionDetail(authPath, authPerms)}; another user could inject credentials.`, - remediation: formatPermissionRemediation({ - targetPath: authPath, - perms: authPerms, - isDir: false, - posixMode: 0o600, - env: params.env, - }), - }); - } else if (authPerms.worldReadable || authPerms.groupReadable) { - findings.push({ - checkId: "fs.auth_profiles.perms_readable", - severity: "warn", - title: "auth-profiles.json is readable by others", - detail: `${formatPermissionDetail(authPath, authPerms)}; auth-profiles.json contains API keys and OAuth tokens.`, - remediation: formatPermissionRemediation({ - targetPath: authPath, - perms: authPerms, - isDir: false, - posixMode: 0o600, - env: params.env, - }), - }); - } - } - const agentDbPath = path.join( params.stateDir, "agents", diff --git a/src/security/fix.test.ts b/src/security/fix.test.ts index 31e85dbe859..5b39989db83 100644 --- a/src/security/fix.test.ts +++ b/src/security/fix.test.ts @@ -239,7 +239,7 @@ describe("security fix", () => { await expectTightenedStateAndConfigPerms(stateDir, configPath); }); - it("collects permission targets for credentials + agent auth/sessions + include files", async () => { + it("collects permission targets for credentials + SQLite state + include files", async () => { const stateDir = await createStateDir("includes"); const includesDir = path.join(stateDir, "includes"); @@ -268,9 +268,6 @@ describe("security fix", () => { const agentDir = path.join(stateDir, "agents", "main", "agent"); await fs.mkdir(agentDir, { recursive: true }); - const authProfilesPath = path.join(agentDir, "auth-profiles.json"); - await fs.writeFile(authProfilesPath, "{}\n", "utf-8"); - await fs.chmod(authProfilesPath, 0o644); const stateDbDir = path.join(stateDir, "state"); await fs.mkdir(stateDbDir, { recursive: true }); @@ -304,7 +301,6 @@ describe("security fix", () => { { path: configPath, mode: 0o600, require: "file" }, { path: credsDir, mode: 0o700, require: "dir" }, { path: allowFromPath, mode: 0o600, require: "file" }, - { path: authProfilesPath, mode: 0o600, require: "file" }, { path: stateDbDir, mode: 0o700, require: "dir" }, { path: stateDbPath, mode: 0o600, require: "file" }, { path: stateWalPath, mode: 0o600, require: "file" }, diff --git a/src/security/fix.ts b/src/security/fix.ts index dc5c6b09125..1d2878bd7e1 100644 --- a/src/security/fix.ts +++ b/src/security/fix.ts @@ -371,8 +371,6 @@ export async function collectSecurityPermissionTargets(params: { targets.push({ path: agentRoot, mode: 0o700, require: "dir" }); targets.push({ path: agentDir, mode: 0o700, require: "dir" }); - const authPath = path.join(agentDir, "auth-profiles.json"); - targets.push({ path: authPath, mode: 0o600, require: "file" }); addSqlitePermissionTargets( targets, resolveOpenClawAgentSqlitePath({ agentId: normalizedAgentId, env: params.env }),