refactor: make auth profile runtime sqlite-addressed

This commit is contained in:
Peter Steinberger
2026-05-10 00:00:18 +01:00
parent 2efadfa722
commit eeaa7101ae
29 changed files with 158 additions and 182 deletions

View File

@@ -22,8 +22,9 @@ export {
resolveAuthProfileOrder,
} from "./auth-profiles/order.js";
export {
resolveAuthProfileStoreAgentDir,
resolveAuthProfileStoreLocationForDisplay,
resolveAuthStatePathForDisplay,
resolveAuthStorePathForDisplay,
} from "./auth-profiles/paths.js";
export {
dedupeProfileIds,

View File

@@ -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;

View File

@@ -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<ResolvedOAuthAccess | null> {
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,

View File

@@ -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";

View File

@@ -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);
}

View File

@@ -1,4 +1,7 @@
export {
resolveAuthProfileStoreAgentDir,
resolveAuthProfileStoreKey,
resolveAuthProfileStoreLocationForDisplay,
resolveAuthStatePath,
resolveAuthStatePathForDisplay,
resolveAuthStorePath,

View File

@@ -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";

View File

@@ -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<string, AuthProfileStore>();
function resolveRuntimeStoreKey(agentDir?: string): string {
return resolveAuthStorePath(agentDir);
return resolveAuthProfileStoreKey(agentDir);
}
export function getRuntimeAuthProfileStoreSnapshot(

View File

@@ -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;

View File

@@ -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,
});

View File

@@ -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 <id>")}) or copy only portable static auth profiles from the main agentDir.`,
].join(" "),
);

View File

@@ -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", () => ({

View File

@@ -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))}`,
};
}

View File

@@ -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", () => {

View File

@@ -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)`);

View File

@@ -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 <name>", "Provider id (e.g. anthropic)")
.option("--profile-id <id>", "Auth profile id (default: <provider>:manual)")
.option(

View File

@@ -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) &&

View File

@@ -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",
});
});

View File

@@ -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,

View File

@@ -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(

View File

@@ -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",
});
},
);

View File

@@ -66,7 +66,7 @@ export {
formatAuthDoctorHint,
resolveAuthProfileEligibility,
resolveAuthProfileOrder,
resolveAuthStorePathForDisplay,
resolveAuthProfileStoreLocationForDisplay,
} from "../agents/auth-profiles.js";
export type {
ApiKeyCredential,

View File

@@ -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<void> {
await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}
function writeAuthProfileStore(fixture: AuditFixture, profiles: Record<string, unknown>): 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<void> {
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 });

View File

@@ -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: "<root>",
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)) {

View File

@@ -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<string>();
for (const agentDir of listAuthProfileStoreAgentDirs(config, stateDir)) {
paths.add(resolveUserPath(resolveAuthStorePath(agentDir)));
}
return [...paths];
}

View File

@@ -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<string, unknown> {
@@ -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[] {

View File

@@ -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",

View File

@@ -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" },

View File

@@ -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 }),