openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
230 lines (229 loc) • 12.4 kB
JavaScript
import { c as isRecord } from "./record-coerce-DItp3I4t.js";
import { m as shortenHomePath } from "./utils-P__uGsPB.js";
import { o as listAgentIds, u as resolveAgentDir, v as resolveDefaultAgentDir } from "./agent-scope-config-DcbEhP0R.js";
import { r as normalizeProviderId } from "./provider-id-DMd-TDFp.js";
import { w as resolveStateDir } from "./paths-D2sRr1a_.js";
import "./agent-scope-DbtJyKUL.js";
import { c as resolveSharedMainAuthAgentDir } from "./path-resolve-oRkRBkQd.js";
import { a as loadPersistedAuthProfileStore, c as mergeAuthProfileStores, s as loadPersistedSharedAuthProfileStore, y as removeRuntimeExternalProfileReferences } from "./persisted-B_qhhBlh.js";
import { S as updateAuthProfileStoreWithLock } from "./store-F1B2duCT.js";
import { c as isNonSecretApiKeyMarker } from "./model-auth-markers-jBKQn38x.js";
import { t as parseModelCatalogJson } from "./model-catalog-json-CxUsDrkg.js";
import { i as isGeneratedPluginModelCatalog, s as loadPersistedPluginModelCatalogsReadOnly } from "./plugin-model-catalog-MRmefceV.js";
import { v as resolveProviderConfigSecretInput } from "./model-auth-provider-config-C_kr_q2g.js";
import { t as note } from "./note-DqHk3fA1.js";
import { t as listAgentModelsJsonPaths } from "./storage-scan-BMZL5YXA.js";
import fs from "node:fs";
import { isDeepStrictEqual } from "node:util";
import path from "node:path";
//#region src/commands/doctor-model-catalog-credentials.ts
/** Doctor-owned migration of plaintext model-catalog credentials into agent SQLite. */
function emptyStore() {
return {
version: 1,
profiles: {}
};
}
function credentialMatches(credential, { provider, key }) {
if (normalizeProviderId(credential?.provider ?? "") !== normalizeProviderId(provider)) return false;
return credential?.type === "api_key" && credential.key === key || credential?.type === "token" && credential.token === key;
}
function matchesProviderEnvRefMarker(cfg, provider, value) {
const ref = resolveProviderConfigSecretInput(cfg, provider).ref;
if (ref?.source !== "env") return false;
const id = ref.id.trim();
const candidate = value.trim();
return candidate === id || candidate === `$${id}` || candidate === `\${${id}}`;
}
function findProviderSecretRefProfiles(store, cfg) {
return Object.entries(store.profiles).filter(([, credential]) => credential.type === "api_key" && credential.key && matchesProviderEnvRefMarker(cfg, credential.provider, credential.key));
}
function collectCredentials(providers, store, blockedStores = [], cfg) {
if (!isRecord(providers)) return [];
return Object.entries(providers).flatMap(([provider, entry]) => {
if (!isRecord(entry) || typeof entry.apiKey !== "string") return [];
const key = entry.apiKey;
const credential = {
provider,
key
};
if (!key.trim() || cfg && resolveProviderConfigSecretInput(cfg, provider).ref || isNonSecretApiKeyMarker(key) || store.profiles[key] !== void 0 || findMatchingProfileId(store, credential, blockedStores) !== void 0) return [];
return [credential];
});
}
function uniqueCredentials(credentials) {
return [...new Map(credentials.map((credential) => [`${normalizeProviderId(credential.provider)}\0${credential.key}`, credential])).values()];
}
function findMatchingProfileId(store, credential, blockedStores) {
return Object.entries(store.profiles).find(([profileId, stored]) => credentialMatches(stored, credential) && blockedStores.every((blocked) => blocked.profiles[profileId] === void 0 || credentialMatches(blocked.profiles[profileId], credential)))?.[0];
}
function allocateProfileId(store, credential, blockedStores) {
const provider = normalizeProviderId(credential.provider);
for (let suffix = 1;; suffix += 1) {
const profileId = suffix === 1 ? `${provider}:default` : `${provider}:models-json${suffix === 2 ? "" : `-${suffix}`}`;
if ((!store.profiles[profileId] || credentialMatches(store.profiles[profileId], credential)) && blockedStores.every((blocked) => !blocked.profiles[profileId] || credentialMatches(blocked.profiles[profileId], credential))) return profileId;
}
}
async function persistCredentials(params) {
const credentials = uniqueCredentials(params.credentials);
const { invalidProfiles } = params;
if (credentials.length === 0 && invalidProfiles.length === 0) return {
migrated: 0,
removed: 0
};
const blockedStores = params.blockedStores ?? [];
const profileIds = /* @__PURE__ */ new Map();
let added = 0;
let removedProfiles = [];
if (!await updateAuthProfileStoreWithLock({
agentDir: params.agentDir,
stateDir: params.stateDir,
saveOptions: {
filterExternalAuthProfiles: false,
syncExternalCli: false
},
updater: (localStore) => {
removedProfiles = invalidProfiles.filter(([profileId, credential]) => isDeepStrictEqual(localStore.profiles[profileId], credential));
if (removedProfiles.length > 0) Object.assign(localStore, removeRuntimeExternalProfileReferences({
store: localStore,
profileIds: new Set(removedProfiles.map(([profileId]) => profileId))
}));
const effectiveStore = params.inheritedStore ? mergeAuthProfileStores(params.inheritedStore, localStore) : localStore;
for (const credential of credentials) {
const profileId = findMatchingProfileId(effectiveStore, credential, blockedStores) ?? allocateProfileId(effectiveStore, credential, blockedStores);
profileIds.set(profileId, credential);
if (credentialMatches(effectiveStore.profiles[profileId], credential)) continue;
localStore.profiles[profileId] = {
type: "api_key",
provider: normalizeProviderId(credential.provider),
key: credential.key
};
effectiveStore.profiles[profileId] = localStore.profiles[profileId];
added += 1;
}
return added > 0 || removedProfiles.length > 0;
}
})) throw new Error("auth profile store could not be updated");
const persisted = params.agentDir ? loadPersistedAuthProfileStore(params.agentDir) : loadPersistedSharedAuthProfileStore({
...process.env,
OPENCLAW_STATE_DIR: params.stateDir
});
const effectivePersisted = params.inheritedStore ? mergeAuthProfileStores(params.inheritedStore, persisted ?? emptyStore()) : persisted;
for (const [profileId, credential] of removedProfiles) if (isDeepStrictEqual(persisted?.profiles[profileId], credential)) throw new Error(`credential cleanup verification failed for profile "${profileId}"`);
for (const [profileId, credential] of profileIds) if (!credentialMatches(effectivePersisted?.profiles[profileId], credential)) throw new Error(`credential verification failed for provider "${credential.provider}"`);
return {
migrated: added,
removed: removedProfiles.length
};
}
function collectAgentCatalogs(agentDir, warnings) {
const localStore = loadPersistedAuthProfileStore(agentDir) ?? emptyStore();
const providers = [];
const rootPath = path.join(agentDir, "models.json");
try {
const root = parseModelCatalogJson(fs.readFileSync(rootPath, "utf8"));
if (isRecord(root) && isRecord(root.providers)) providers.push(root.providers);
} catch (error) {
if (error.code !== "ENOENT") warnings.push(`Could not read model catalog ${shortenHomePath(rootPath)}: ${error instanceof Error ? error.message : String(error)}`);
}
try {
for (const catalog of loadPersistedPluginModelCatalogsReadOnly(agentDir)) try {
const parsed = JSON.parse(catalog.contents);
if (isGeneratedPluginModelCatalog(parsed) && isRecord(parsed) && isRecord(parsed.providers)) providers.push(parsed.providers);
} catch {
warnings.push(`Could not parse generated model catalog for plugin ${catalog.pluginId}.`);
}
} catch (error) {
warnings.push(`Could not read generated model catalogs for ${shortenHomePath(agentDir)}: ${error instanceof Error ? error.message : String(error)}`);
}
return {
agentDir,
localStore,
providers
};
}
/** Copies and verifies catalog credentials before the runtime retires plaintext catalog auth. */
async function maybeMigrateModelCatalogCredentials(params) {
const warnings = [];
const env = params.env ?? process.env;
const stateDir = resolveStateDir(env);
const mainAgentDir = resolveSharedMainAuthAgentDir(env);
const discoveredAgentDirs = listAgentModelsJsonPaths(params.cfg, stateDir, env).map((modelsPath) => path.dirname(modelsPath));
const agentIds = listAgentIds(params.cfg);
const configuredAgentDirs = agentIds.length > 0 ? agentIds.map((agentId) => resolveAgentDir(params.cfg, agentId, env)) : [resolveDefaultAgentDir(params.cfg, env)];
const agentDirs = [.../* @__PURE__ */ new Set([
mainAgentDir,
...configuredAgentDirs,
...discoveredAgentDirs
])];
const mainStore = loadPersistedSharedAuthProfileStore(env) ?? emptyStore();
const catalogs = agentDirs.map((agentDir) => collectAgentCatalogs(agentDir, warnings));
const effectiveStores = catalogs.map(({ localStore }) => mergeAuthProfileStores(mainStore, localStore));
const childStores = catalogs.filter((catalog) => catalog.agentDir !== mainAgentDir).map((catalog) => catalog.localStore);
const configCredentials = collectCredentials(params.cfg.models?.providers, mainStore, childStores, params.cfg);
const catalogCredentials = catalogs.map((catalog, index) => uniqueCredentials(catalog.providers.flatMap((providers) => collectCredentials(providers, effectiveStores[index] ?? mainStore, [], params.cfg))));
const invalidMainProfiles = findProviderSecretRefProfiles(mainStore, params.cfg);
const invalidCatalogProfiles = catalogs.map((catalog) => catalog.agentDir === mainAgentDir ? [] : findProviderSecretRefProfiles(catalog.localStore, params.cfg));
const detected = configCredentials.length + catalogCredentials.reduce((sum, entries) => sum + entries.length, 0);
const removable = invalidMainProfiles.length + invalidCatalogProfiles.reduce((sum, profiles) => sum + profiles.length, 0);
for (const warning of warnings) params.runtime.error(warning);
if (detected === 0 && removable === 0) return {
detected,
migrated: 0,
removed: 0,
warnings
};
if (detected > 0) note(`Found ${detected} plaintext model credential${detected === 1 ? "" : "s"}. Run openclaw doctor --fix to copy and verify them in agent SQLite before plaintext catalog authentication is retired.`, "Model catalog credentials");
if (removable > 0) note(`Found ${removable} stored unresolved model credential marker${removable === 1 ? "" : "s"}. Run openclaw doctor --fix to remove them from agent SQLite.`, "Model catalog credentials");
if (!(params.prompter.shouldRepair || await params.prompter.confirmAutoFix({
message: "Repair model credentials in agent SQLite now?",
initialValue: true
}))) return {
detected,
migrated: 0,
removed: 0,
warnings
};
let migrated = 0;
let removed = 0;
try {
const result = await persistCredentials({
blockedStores: childStores,
credentials: configCredentials,
invalidProfiles: invalidMainProfiles,
stateDir
});
migrated += result.migrated;
removed += result.removed;
} catch (error) {
const warning = `Could not migrate configured model credentials: ${error instanceof Error ? error.message : String(error)}`;
warnings.push(warning);
params.runtime.error(warning);
}
const migratedMainStore = loadPersistedSharedAuthProfileStore(env) ?? mainStore;
for (const [index, catalog] of catalogs.entries()) try {
const result = await persistCredentials({
...catalog.agentDir === mainAgentDir ? {} : { agentDir: catalog.agentDir },
credentials: catalogCredentials[index] ?? [],
invalidProfiles: invalidCatalogProfiles[index] ?? [],
...catalog.agentDir === mainAgentDir ? {} : { inheritedStore: migratedMainStore },
stateDir
});
migrated += result.migrated;
removed += result.removed;
} catch (error) {
const warning = `Could not migrate model credentials for ${shortenHomePath(catalog.agentDir)}: ${error instanceof Error ? error.message : String(error)}`;
warnings.push(warning);
params.runtime.error(warning);
}
if (migrated > 0) note(`Copied and verified ${migrated} model credential${migrated === 1 ? "" : "s"} in agent SQLite. Existing catalog values remain active until the runtime migration lands.`, "Doctor changes");
if (removed > 0) note(`Removed ${removed} unresolved model credential marker${removed === 1 ? "" : "s"} from agent SQLite.`, "Doctor changes");
return {
detected,
migrated,
removed,
warnings
};
}
//#endregion
export { maybeMigrateModelCatalogCredentials };