UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

816 lines (815 loc) 38.7 kB
import { o as isRecord } from "./record-coerce-DHZ4bFlT.js"; import { y as resolveStateDir } from "./paths-mvMm5bYV.js"; import { t as formatCliCommand } from "./command-format-CKGmlpAQ.js"; import { g as shortenHomePath } from "./utils-CCC-BEJH.js"; import { o as coerceSecretRef } from "./types.secrets-_0JOMGE5.js"; import "./agent-scope-MrLta7Pq.js"; import { a as resolveAgentDir, n as listAgentIds, s as resolveDefaultAgentDir } from "./agent-scope-config-CgCYpZfK.js"; import { d as resolveLegacyAuthStorePath, l as resolveAuthStorePath, s as resolveAuthStatePath } from "./runtime-snapshots-CGKcj2Tz.js"; import { _ as loadLegacyAuthProfileStore, g as coercePersistedAuthProfileStore, h as applyLegacyAuthStore, p as saveAuthProfileStore, t as clearRuntimeAuthProfileStoreSnapshots, v as loadPersistedAuthProfileStore, y as coerceAuthProfileState } from "./store-C8spD0DG.js"; import { t as loadJsonFile } from "./json-file-CVAOif1i.js"; import { t as note } from "./note-BRSJp0UF.js"; import fs from "node:fs"; import path from "node:path"; //#region src/commands/doctor-auth-flat-profiles.ts /** Doctor repairs for legacy auth profile JSON stores and OpenAI provider-id migrations. */ const UNSAFE_LEGACY_AUTH_PROFILE_KEYS = new Set([ "__proto__", "constructor", "prototype" ]); function readNonEmptyString(value) { return typeof value === "string" && value.trim() ? value : void 0; } function isSafeLegacyProviderKey(key) { return key.trim().length > 0 && !UNSAFE_LEGACY_AUTH_PROFILE_KEYS.has(key); } function extractProviderFromProfileId(profileId) { const colon = profileId.indexOf(":"); if (colon <= 0) return; return readNonEmptyString(profileId.slice(0, colon)); } function inferLegacyCredentialType(record) { const explicit = readNonEmptyString(record.type) ?? readNonEmptyString(record.mode); if (explicit === "api_key" || explicit === "token" || explicit === "oauth") return explicit; if (readNonEmptyString(record.key) ?? readNonEmptyString(record.apiKey)) return "api_key"; if (readNonEmptyString(record.token)) return "token"; if (readNonEmptyString(record.access) && readNonEmptyString(record.refresh) && typeof record.expires === "number") return "oauth"; } function coerceLegacyFlatCredential(providerId, raw) { if (!isRecord(raw)) return null; const provider = readNonEmptyString(raw.provider) ?? providerId; const type = inferLegacyCredentialType(raw); const email = readNonEmptyString(raw.email); if (type === "api_key") { const key = readNonEmptyString(raw.key) ?? readNonEmptyString(raw.apiKey); return key ? { type, provider, key, ...email ? { email } : {} } : null; } if (type === "token") { const token = readNonEmptyString(raw.token); return token ? { type, provider, token, ...typeof raw.expires === "number" ? { expires: raw.expires } : {}, ...email ? { email } : {} } : null; } if (type === "oauth") { const access = readNonEmptyString(raw.access); const refresh = readNonEmptyString(raw.refresh); if (!access || !refresh || typeof raw.expires !== "number") return null; return { type, provider, access, refresh, expires: raw.expires, ...readNonEmptyString(raw.enterpriseUrl) ? { enterpriseUrl: readNonEmptyString(raw.enterpriseUrl) } : {}, ...readNonEmptyString(raw.projectId) ? { projectId: readNonEmptyString(raw.projectId) } : {}, ...readNonEmptyString(raw.accountId) ? { accountId: readNonEmptyString(raw.accountId) } : {}, ...email ? { email } : {} }; } return null; } function coerceLegacyFlatAuthProfileStore(raw) { if (!isRecord(raw) || "profiles" in raw) return null; const store = { version: 1, profiles: {} }; for (const [key, value] of Object.entries(raw)) { const providerId = key.trim(); if (!isSafeLegacyProviderKey(providerId)) continue; const credential = coerceLegacyFlatCredential(providerId, value); if (!credential) continue; store.profiles[`${providerId}:default`] = credential; } return Object.keys(store.profiles).length > 0 ? store : null; } function addCandidate(candidates, agentDir) { const authPath = resolveAuthStorePath(agentDir); candidates.set(path.resolve(authPath), { agentDir, authPath }); } function listExistingAgentDirsFromState(env) { const root = path.join(resolveStateDir(env), "agents"); let entries; try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { return []; } return entries.filter((entry) => entry.isDirectory()).map((entry) => path.join(root, entry.name, "agent")).filter((agentDir) => { try { return fs.statSync(agentDir).isDirectory(); } catch { return false; } }); } function listAuthProfileRepairCandidates(cfg, env) { const candidates = /* @__PURE__ */ new Map(); addCandidate(candidates, resolveDefaultAgentDir(cfg, env)); const envAgentDir = readNonEmptyString(env.OPENCLAW_AGENT_DIR) ?? readNonEmptyString(env.PI_CODING_AGENT_DIR); if (envAgentDir) addCandidate(candidates, envAgentDir); for (const agentId of listAgentIds(cfg)) addCandidate(candidates, resolveAgentDir(cfg, agentId, env)); for (const agentDir of listExistingAgentDirsFromState(env)) addCandidate(candidates, agentDir); return [...candidates.values()]; } function listAuthProfileSqliteMigrationCandidates(cfg, env) { const candidates = []; for (const candidate of listAuthProfileRepairCandidates(cfg, env)) candidates.push({ agentDir: candidate.agentDir, authPath: candidate.authPath, statePath: resolveAuthStatePath(candidate.agentDir), legacyPath: resolveLegacyAuthStorePath(candidate.agentDir) }); return candidates; } function hasAuthProfileState(state) { return Boolean(state.order || state.lastGood || state.usageStats); } function normalizeLegacyApiKeyAliasesForImport(raw) { if (!isRecord(raw) || !isRecord(raw.profiles)) return; for (const profile of Object.values(raw.profiles)) { if (!isRecord(profile)) continue; if ((readNonEmptyString(profile.type) ?? readNonEmptyString(profile.mode)) !== "api_key") continue; if (readNonEmptyString(profile.key) !== void 0 || coerceSecretRef(profile.keyRef) !== null || profile["api_key"] === void 0) continue; profile.key = profile["api_key"]; } } function collectAuthProfileStateProfileIds(state) { const profileIds = /* @__PURE__ */ new Set(); for (const entries of Object.values(state.order ?? {})) for (const profileId of entries) profileIds.add(profileId); for (const profileId of Object.values(state.lastGood ?? {})) profileIds.add(profileId); for (const profileId of Object.keys(state.usageStats ?? {})) profileIds.add(profileId); return [...profileIds]; } function mergeImportedAuthProfiles(params) { const profiles = { ...params.store.profiles }; for (const [profileId, credential] of Object.entries(params.profiles)) if (!params.existingProfileIds.has(profileId)) profiles[profileId] = credential; return { ...params.store, profiles }; } function mergeImportedAuthProfileState(params) { return { ...params.store, ...params.state.order ? { order: { ...params.store.order, ...Object.fromEntries(Object.entries(params.state.order).filter(([provider]) => !params.existingState.order?.[provider])) } } : {}, ...params.state.lastGood ? { lastGood: { ...params.store.lastGood, ...Object.fromEntries(Object.entries(params.state.lastGood).filter(([provider]) => !params.existingState.lastGood?.[provider])) } } : {}, ...params.state.usageStats ? { usageStats: { ...params.store.usageStats, ...Object.fromEntries(Object.entries(params.state.usageStats).filter(([profileId]) => !params.existingState.usageStats?.[profileId])) } } : {} }; } function formatMissingAuthProfileSqliteVerification(params) { const missingProfileIds = [...params.importedProfileIds].filter((profileId) => !params.loaded?.profiles[profileId]); const missingStateFields = []; for (const [provider, profileIds] of Object.entries(params.expected.order ?? {})) { const loadedProfileIds = params.loaded?.order?.[provider]; if (!loadedProfileIds || loadedProfileIds.length !== profileIds.length || loadedProfileIds.some((profileId, index) => profileId !== profileIds[index])) missingStateFields.push(`order.${provider}`); } for (const [provider, profileId] of Object.entries(params.expected.lastGood ?? {})) if (params.loaded?.lastGood?.[provider] !== profileId) missingStateFields.push(`lastGood.${provider}`); for (const profileId of Object.keys(params.expected.usageStats ?? {})) if (!params.loaded?.usageStats?.[profileId]) missingStateFields.push(`usageStats.${profileId}`); const parts = []; if (missingProfileIds.length > 0) parts.push(`imported profile(s): ${missingProfileIds.toSorted().join(", ")}`); if (missingStateFields.length > 0) parts.push(`auth state field(s): ${missingStateFields.toSorted().join(", ")}`); return parts.length > 0 ? parts.join("; ") : null; } function filterRawAuthProfileState(raw, shouldKeepProfileId) { if (isRecord(raw.order)) { for (const [provider, profileIds] of Object.entries(raw.order)) { if (!Array.isArray(profileIds)) continue; const kept = profileIds.filter((profileId) => typeof profileId === "string" && shouldKeepProfileId(profileId)); if (kept.length > 0) raw.order[provider] = kept; else delete raw.order[provider]; } if (Object.keys(raw.order).length === 0) delete raw.order; } if (isRecord(raw.lastGood)) { for (const [provider, profileId] of Object.entries(raw.lastGood)) if (typeof profileId !== "string" || !shouldKeepProfileId(profileId)) delete raw.lastGood[provider]; if (Object.keys(raw.lastGood).length === 0) delete raw.lastGood; } if (isRecord(raw.usageStats)) { for (const profileId of Object.keys(raw.usageStats)) if (!shouldKeepProfileId(profileId)) delete raw.usageStats[profileId]; if (Object.keys(raw.usageStats).length === 0) delete raw.usageStats; } } function pruneRawAuthProfileIds(raw, profileIds) { if (!isRecord(raw) || !isRecord(raw.profiles)) return; for (const profileId of profileIds) delete raw.profiles[profileId]; filterRawAuthProfileState(raw, (profileId) => !profileIds.has(profileId)); } function pickRawAuthProfileIds(raw, profileIds) { if (!isRecord(raw) || !isRecord(raw.profiles)) return null; const profiles = Object.fromEntries(Object.entries(raw.profiles).filter(([profileId]) => profileIds.has(profileId))); if (Object.keys(profiles).length === 0) return null; const next = structuredClone(raw); next.profiles = profiles; filterRawAuthProfileState(next, (profileId) => profileIds.has(profileId)); return next; } function collectUnresolvedLegacyOAuthSidecarProfileIds(raw) { if (!isRecord(raw) || !isRecord(raw.profiles)) return []; const profileIds = []; for (const [profileId, profile] of Object.entries(raw.profiles)) { if (!isRecord(profile) || profile.type !== "oauth" || !isRecord(profile.oauthRef)) continue; if (readNonEmptyString(profile.oauthRef.id) && readNonEmptyString(profile.oauthRef.provider) && (!readNonEmptyString(profile.access) || !readNonEmptyString(profile.refresh))) profileIds.push(profileId); } return profileIds; } function hasImportableAuthProfileStore(store) { return Boolean(store && (Object.keys(store.profiles).length > 0 || hasAuthProfileState(store))); } function backupAuthProfileJson(pathname, suffix, now) { const backupPath = `${pathname}.${suffix}.${now()}.bak`; fs.copyFileSync(pathname, backupPath); return backupPath; } function backupAndRemoveAuthProfileJson(pathname, suffix, now) { const backupPath = backupAuthProfileJson(pathname, suffix, now); fs.unlinkSync(pathname); return backupPath; } function writeJsonFile(pathname, value) { fs.writeFileSync(pathname, `${JSON.stringify(value, null, 2)}\n`, "utf8"); } /** * Imports legacy auth profile JSON and state files into the per-agent SQLite store. * * JSON files are backed up and removed only after import. OAuth profiles that still depend on * unresolved sidecar secrets are kept in JSON so the sidecar migration can run first. */ async function maybeMigrateAuthProfileJsonStoresToSqlite(params) { const now = params.now ?? Date.now; const env = params.env ?? process.env; const loadMigratedStore = params.deps?.loadPersistedAuthProfileStore ?? loadPersistedAuthProfileStore; const detected = listAuthProfileSqliteMigrationCandidates(params.cfg, env).filter((candidate) => fs.existsSync(candidate.authPath) || fs.existsSync(candidate.statePath) || fs.existsSync(candidate.legacyPath)); const result = { detected: detected.flatMap((candidate) => [ candidate.authPath, candidate.statePath, candidate.legacyPath ].filter((pathname) => fs.existsSync(pathname))), changes: [], warnings: [] }; if (detected.length === 0) return result; note([...detected.map((candidate) => `- ${shortenHomePath(candidate.authPath)} / ${shortenHomePath(candidate.statePath)}`), `- ${formatCliCommand("openclaw doctor --fix")} imports legacy auth profile JSON into the per-agent SQLite database and removes the old files after backup.`].join("\n"), "Auth profile SQLite migration"); if (!await params.prompter.confirmAutoFix({ message: "Migrate auth profile JSON files into SQLite now?", initialValue: true })) return result; for (const candidate of detected) try { const rawStore = fs.existsSync(candidate.authPath) ? loadJsonFile(candidate.authPath) : null; const unresolvedSidecarProfileIds = new Set(collectUnresolvedLegacyOAuthSidecarProfileIds(rawStore)); const unresolvedSidecarRawStore = unresolvedSidecarProfileIds.size > 0 ? pickRawAuthProfileIds(rawStore, unresolvedSidecarProfileIds) : null; if (unresolvedSidecarProfileIds.size > 0) { pruneRawAuthProfileIds(rawStore, unresolvedSidecarProfileIds); result.warnings.push(`Left ${unresolvedSidecarProfileIds.size} legacy OAuth sidecar profile${unresolvedSidecarProfileIds.size === 1 ? "" : "s"} in ${shortenHomePath(candidate.authPath)}; rerun ${formatCliCommand("openclaw doctor --fix")} after sidecar migration or re-authenticate those profiles.`); } const awsSdkMarkerStore = isRecord(rawStore) && isRecord(rawStore.profiles) ? resolveAwsSdkAuthProfileMarkerStore(candidate) : null; if (awsSdkMarkerStore && isRecord(rawStore)) { const configProfiles = ensureConfigAuthProfiles(params.cfg); for (const marker of awsSdkMarkerStore.profiles) configProfiles[marker.profileId] = { provider: marker.provider, mode: "aws-sdk", ...marker.email ? { email: marker.email } : {}, ...marker.displayName ? { displayName: marker.displayName } : {} }; removeAwsSdkProfileMarkers(rawStore, awsSdkMarkerStore.profiles.map((profile) => profile.profileId)); result.configChanged = true; } normalizeLegacyApiKeyAliasesForImport(rawStore); const maybeCanonicalStore = coercePersistedAuthProfileStore(rawStore) ?? coerceLegacyFlatAuthProfileStore(rawStore) ?? null; const canonicalStore = hasImportableAuthProfileStore(maybeCanonicalStore) ? maybeCanonicalStore : null; const legacyStore = loadLegacyAuthProfileStore(candidate.agentDir); const state = coerceAuthProfileState(fs.existsSync(candidate.statePath) ? loadJsonFile(candidate.statePath) : null); if (!canonicalStore && !legacyStore && !hasAuthProfileState(state) && !awsSdkMarkerStore) { result.warnings.push(`Left auth profile JSON in place for ${shortenHomePath(candidate.authPath)} because no importable auth profiles or state were found.`); continue; } const existing = loadMigratedStore(candidate.agentDir) ?? { version: 1, profiles: {} }; const existingProfileIds = new Set(Object.keys(existing.profiles)); const existingState = coerceAuthProfileState(existing); let next = { ...existing }; const importedProfileIds = /* @__PURE__ */ new Set(); if (legacyStore) { const legacyAsStore = { version: 1, profiles: {} }; applyLegacyAuthStore(legacyAsStore, legacyStore); for (const profileId of Object.keys(legacyAsStore.profiles)) importedProfileIds.add(profileId); next = mergeImportedAuthProfiles({ store: next, profiles: legacyAsStore.profiles, existingProfileIds }); } if (canonicalStore) { for (const profileId of Object.keys(canonicalStore.profiles)) importedProfileIds.add(profileId); next = { ...next, version: Math.max(next.version, canonicalStore.version) }; next = mergeImportedAuthProfiles({ store: next, profiles: canonicalStore.profiles, existingProfileIds }); next = mergeImportedAuthProfileState({ store: next, state: coerceAuthProfileState(canonicalStore), existingState }); } if (hasAuthProfileState(state)) next = mergeImportedAuthProfileState({ store: next, state, existingState }); if (canonicalStore || legacyStore || hasAuthProfileState(state)) { const stateProfileIds = [...collectAuthProfileStateProfileIds(state), ...canonicalStore ? collectAuthProfileStateProfileIds(coerceAuthProfileState(canonicalStore)) : []]; saveAuthProfileStore(next, candidate.agentDir, { filterExternalAuthProfiles: false, preserveStateProfileIds: stateProfileIds, syncExternalCli: false }); const verificationFailure = formatMissingAuthProfileSqliteVerification({ expected: next, importedProfileIds, loaded: loadMigratedStore(candidate.agentDir) }); if (verificationFailure) { result.warnings.push(`Left auth profile JSON in place for ${shortenHomePath(candidate.authPath)} because SQLite verification did not find ${verificationFailure}.`); continue; } } const backups = []; if (fs.existsSync(candidate.authPath)) if (unresolvedSidecarRawStore) { backups.push(backupAuthProfileJson(candidate.authPath, "sqlite-import", now)); writeJsonFile(candidate.authPath, unresolvedSidecarRawStore); } else backups.push(backupAndRemoveAuthProfileJson(candidate.authPath, "sqlite-import", now)); if (fs.existsSync(candidate.statePath)) backups.push(backupAndRemoveAuthProfileJson(candidate.statePath, "sqlite-import", now)); if (fs.existsSync(candidate.legacyPath)) backups.push(backupAndRemoveAuthProfileJson(candidate.legacyPath, "sqlite-import", now)); result.changes.push(`Migrated auth profile JSON for ${shortenHomePath(candidate.authPath)} into SQLite (backup${backups.length === 1 ? "" : "s"}: ${backups.map(shortenHomePath).join(", ")}).`); if (awsSdkMarkerStore) result.changes.push(`Moved aws-sdk profile metadata from ${shortenHomePath(candidate.authPath)} to auth.profiles before removing the legacy auth profile JSON.`); } catch (err) { result.warnings.push(`Failed to migrate auth profile JSON for ${shortenHomePath(candidate.authPath)}: ${String(err)}`); } clearRuntimeAuthProfileStoreSnapshots(); if (result.changes.length > 0) note(result.changes.map((change) => `- ${change}`).join("\n"), "Doctor changes"); if (result.warnings.length > 0) note(result.warnings.map((warning) => `- ${warning}`).join("\n"), "Doctor warnings"); return result; } function resolveLegacyFlatStore(candidate) { if (!fs.existsSync(candidate.authPath)) return null; const raw = loadJsonFile(candidate.authPath); if (!raw || typeof raw !== "object" || "profiles" in raw) return null; const store = coerceLegacyFlatAuthProfileStore(raw); if (!store || Object.keys(store.profiles).length === 0) return null; return { ...candidate, store }; } function backupAuthProfileStore(authPath, now) { const backupPath = `${authPath}.legacy-flat.${now()}.bak`; fs.copyFileSync(authPath, backupPath); return backupPath; } function backupAwsSdkProfileMarkerStore(authPath, now) { const backupPath = `${authPath}.aws-sdk-profile.${now()}.bak`; fs.copyFileSync(authPath, backupPath); return backupPath; } function resolveAwsSdkAuthProfileMarkerStore(candidate) { if (!fs.existsSync(candidate.authPath)) return null; const raw = loadJsonFile(candidate.authPath); if (!isRecord(raw) || !isRecord(raw.profiles)) return null; const markers = []; for (const [profileId, value] of Object.entries(raw.profiles)) { if (!isRecord(value)) continue; if ((readNonEmptyString(value.type) ?? readNonEmptyString(value.mode)) !== "aws-sdk") continue; const provider = readNonEmptyString(value.provider) ?? extractProviderFromProfileId(profileId); if (!provider || !isSafeLegacyProviderKey(provider)) continue; markers.push({ profileId, provider, ...readNonEmptyString(value.email) ? { email: readNonEmptyString(value.email) } : {}, ...readNonEmptyString(value.displayName) ? { displayName: readNonEmptyString(value.displayName) } : {} }); } return markers.length > 0 ? { ...candidate, raw, profiles: markers } : null; } function ensureConfigAuthProfiles(config) { const root = config; const auth = isRecord(root.auth) ? root.auth : {}; if (root.auth !== auth) root.auth = auth; if (!isRecord(auth.profiles)) auth.profiles = {}; return auth.profiles; } function removeAwsSdkProfileMarkers(raw, profileIds) { if (!isRecord(raw.profiles)) return; for (const profileId of profileIds) delete raw.profiles[profileId]; } /** * Rewrites pre-versioned flat auth profile JSON into canonical profile stores. * * Also lifts aws-sdk profile markers into config because those entries are routing metadata, not * credentials, and the runtime no longer treats them as stored secrets. */ async function maybeRepairLegacyFlatAuthProfileStores(params) { const now = params.now ?? Date.now; const env = params.env ?? process.env; const legacyStores = listAuthProfileRepairCandidates(params.cfg, env).map(resolveLegacyFlatStore).filter((entry) => entry !== null); const awsSdkMarkerStores = listAuthProfileRepairCandidates(params.cfg, env).map(resolveAwsSdkAuthProfileMarkerStore).filter((entry) => entry !== null); const result = { detected: [...legacyStores.map((entry) => entry.authPath), ...awsSdkMarkerStores.map((entry) => entry.authPath)], changes: [], warnings: [] }; if (legacyStores.length === 0 && awsSdkMarkerStores.length === 0) return result; const noteLines = [...legacyStores.map((entry) => `- ${shortenHomePath(entry.authPath)} uses the legacy flat auth profile format.`), ...awsSdkMarkerStores.map((entry) => `- ${shortenHomePath(entry.authPath)} contains aws-sdk profile markers that belong in openclaw.json auth.profiles.`)]; if (legacyStores.length > 0) noteLines.push(`- The gateway expects the canonical version/profiles store; ${formatCliCommand("openclaw doctor --fix")} rewrites this legacy shape with a backup.`); if (awsSdkMarkerStores.length > 0) noteLines.push(`- AWS SDK profile markers are routing metadata, not stored credentials; ${formatCliCommand("openclaw doctor --fix")} moves them to config with a backup.`); note(noteLines.join("\n"), "Auth profiles"); if (!await params.prompter.confirmAutoFix({ message: "Repair legacy auth-profiles.json files now?", initialValue: true })) return result; for (const entry of legacyStores) try { const backupPath = backupAuthProfileStore(entry.authPath, now); saveAuthProfileStore(entry.store, entry.agentDir, { syncExternalCli: false }); fs.unlinkSync(entry.authPath); result.changes.push(`Migrated ${shortenHomePath(entry.authPath)} to the SQLite auth profile store (backup: ${shortenHomePath(backupPath)}).`); } catch (err) { result.warnings.push(`Failed to rewrite ${shortenHomePath(entry.authPath)}: ${String(err)}`); } for (const entry of awsSdkMarkerStores) try { const backupPath = backupAwsSdkProfileMarkerStore(entry.authPath, now); const configProfiles = ensureConfigAuthProfiles(params.cfg); for (const marker of entry.profiles) configProfiles[marker.profileId] = { provider: marker.provider, mode: "aws-sdk", ...marker.email ? { email: marker.email } : {}, ...marker.displayName ? { displayName: marker.displayName } : {} }; removeAwsSdkProfileMarkers(entry.raw, entry.profiles.map((profile) => profile.profileId)); fs.writeFileSync(entry.authPath, `${JSON.stringify(entry.raw, null, 2)}\n`); result.changes.push(`Moved aws-sdk profile metadata from ${shortenHomePath(entry.authPath)} to auth.profiles (backup: ${shortenHomePath(backupPath)}).`); } catch (err) { result.warnings.push(`Failed to migrate aws-sdk profile markers from ${shortenHomePath(entry.authPath)}: ${String(err)}`); } clearRuntimeAuthProfileStoreSnapshots(); if (result.changes.length > 0) note(result.changes.map((change) => `- ${change}`).join("\n"), "Doctor changes"); if (result.warnings.length > 0) note(result.warnings.map((warning) => `- ${warning}`).join("\n"), "Doctor warnings"); return result; } function resolveCanonicalApiKeyAliasRepair(candidate) { if (!fs.existsSync(candidate.authPath)) return null; const raw = loadJsonFile(candidate.authPath); if (!isRecord(raw) || !isRecord(raw.profiles)) return null; const profileIds = []; for (const [profileId, value] of Object.entries(raw.profiles)) { if (!isRecord(value)) continue; const type = readNonEmptyString(value.type) ?? readNonEmptyString(value.mode); const hasApiKeyField = readNonEmptyString(value["api_key"]) !== void 0 || coerceSecretRef(value["api_key"]) !== null; const hasCanonicalKey = readNonEmptyString(value.key) !== void 0 || coerceSecretRef(value.key) !== null; const hasCanonicalKeyRef = coerceSecretRef(value.keyRef) !== null; if (type === "api_key" && hasApiKeyField && !hasCanonicalKey && !hasCanonicalKeyRef) profileIds.push(profileId); } return profileIds.length > 0 ? { authPath: candidate.authPath, raw, profileIds } : null; } function backupCanonicalApiKeyAlias(authPath, now) { const backupPath = `${authPath}.api-key-alias.${now()}.bak`; fs.copyFileSync(authPath, backupPath); return backupPath; } /** * Repairs auth profile JSON that used the historical "api_key" credential field. * * Runtime parsing reads "key" or "keyRef"; doctor preserves the original file as a backup before * moving the alias into the canonical key slot. */ async function maybeRepairCanonicalApiKeyFieldAlias(params) { const now = params.now ?? Date.now; const env = params.env ?? process.env; const repairs = listAuthProfileRepairCandidates(params.cfg, env).map(resolveCanonicalApiKeyAliasRepair).filter((entry) => entry !== null); const result = { detected: repairs.map((entry) => entry.authPath), changes: [], warnings: [] }; if (repairs.length === 0) return result; const noteLines = repairs.map((entry) => `- ${shortenHomePath(entry.authPath)} has ${entry.profileIds.length} profile(s) using the non-canonical "api_key" field; the canonical field is "key".`); noteLines.push(`- Runtime auth parsing only reads canonical "key" and "keyRef" fields, so these profiles are silently skipped; ${formatCliCommand("openclaw doctor --fix")} rewrites "api_key" to "key" with a backup.`); note(noteLines.join("\n"), "Auth profiles"); if (!await params.prompter.confirmAutoFix({ message: "Rewrite non-canonical \"api_key\" fields to \"key\" now?", initialValue: true })) return result; for (const entry of repairs) try { const backupPath = backupCanonicalApiKeyAlias(entry.authPath, now); const profiles = entry.raw.profiles; for (const profileId of entry.profileIds) { const profile = profiles[profileId]; if (!isRecord(profile)) continue; profile.key = profile["api_key"]; delete profile["api_key"]; } fs.writeFileSync(entry.authPath, `${JSON.stringify(entry.raw, null, 2)}\n`); result.changes.push(`Rewrote ${entry.profileIds.length} "api_key" field(s) to "key" in ${shortenHomePath(entry.authPath)} (backup: ${shortenHomePath(backupPath)}).`); } catch (err) { result.warnings.push(`Failed to rewrite "api_key" fields in ${shortenHomePath(entry.authPath)}: ${String(err)}`); } clearRuntimeAuthProfileStoreSnapshots(); if (result.changes.length > 0) note(result.changes.map((change) => `- ${change}`).join("\n"), "Doctor changes"); if (result.warnings.length > 0) note(result.warnings.map((warning) => `- ${warning}`).join("\n"), "Doctor warnings"); return result; } const LEGACY_OPENAI_CODEX_PROVIDER_ID = "openai-codex"; const OPENAI_PROVIDER_ID = "openai"; function isLegacyOpenAICodexProvider(value) { return typeof value === "string" && value.trim().toLowerCase() === LEGACY_OPENAI_CODEX_PROVIDER_ID; } function isLegacyOpenAICodexProfileId(profileId) { return profileId.trim().toLowerCase().startsWith(`${LEGACY_OPENAI_CODEX_PROVIDER_ID}:`); } function canonicalOpenAIProfileSuffix(profileId) { return profileId.slice(profileId.indexOf(":") + 1).trim() || "default"; } function allocateOpenAIProfileId(legacyProfileId, occupied) { const suffix = canonicalOpenAIProfileSuffix(legacyProfileId); const direct = `${OPENAI_PROVIDER_ID}:${suffix}`; if (!occupied.has(direct)) { occupied.add(direct); return direct; } const chatgpt = `${OPENAI_PROVIDER_ID}:chatgpt-${suffix}`; if (!occupied.has(chatgpt)) { occupied.add(chatgpt); return chatgpt; } for (let index = 2;; index += 1) { const candidate = `${chatgpt}-${index}`; if (!occupied.has(candidate)) { occupied.add(candidate); return candidate; } } } function canonicalizeOpenAIProfileEntries(profiles, options) { const occupied = new Set(Object.keys(profiles).filter((id) => !isLegacyOpenAICodexProfileId(id))); const reservedMappedIds = new Set(options?.profileIdMap?.values() ?? []); const profileIdMap = /* @__PURE__ */ new Map(); let changed = false; for (const [profileId, rawProfile] of Object.entries({ ...profiles })) { if (!isRecord(rawProfile)) continue; const legacyId = isLegacyOpenAICodexProfileId(profileId); const legacyProvider = isLegacyOpenAICodexProvider(rawProfile.provider); if (!legacyId && !legacyProvider) continue; const mappedProfileId = legacyId ? options?.profileIdMap?.get(profileId) : void 0; const nextProfileId = mappedProfileId && !occupied.has(mappedProfileId) ? mappedProfileId : legacyId ? allocateOpenAIProfileId(profileId, new Set([...occupied, ...reservedMappedIds])) : profileId; occupied.add(nextProfileId); const nextProfile = { ...rawProfile, provider: OPENAI_PROVIDER_ID }; if (nextProfileId !== profileId) { delete profiles[profileId]; profileIdMap.set(profileId, nextProfileId); } profiles[nextProfileId] = nextProfile; changed = true; } return { profileIdMap, changed }; } function replaceMappedProfileId(value, profileIdMap) { if (typeof value === "string") return profileIdMap.get(value) ?? value; if (Array.isArray(value)) { let changed = false; const next = value.map((entry) => { const replaced = replaceMappedProfileId(entry, profileIdMap); changed ||= replaced !== entry; return replaced; }); return changed ? next : value; } if (!isRecord(value)) return value; let changed = false; for (const [key, entry] of Object.entries(value)) { const replaced = replaceMappedProfileId(entry, profileIdMap); if (replaced !== entry) { value[key] = replaced; changed = true; } } return changed ? value : value; } const AUTH_PROFILE_REF_KEYS = new Set(["authProfileId"]); function rewriteMappedAuthProfileRefs(value, profileIdMap) { if (Array.isArray(value)) return value.reduce((changed, entry) => rewriteMappedAuthProfileRefs(entry, profileIdMap) || changed, false); if (!isRecord(value)) return false; let changed = false; for (const [key, entry] of Object.entries(value)) { if (AUTH_PROFILE_REF_KEYS.has(key) && typeof entry === "string") { const replaced = profileIdMap.get(entry); if (replaced && replaced !== entry) { value[key] = replaced; changed = true; } continue; } changed = rewriteMappedAuthProfileRefs(entry, profileIdMap) || changed; } return changed; } function canonicalizeOpenAIAuthOrder(auth, profileIdMap) { if (!isRecord(auth.order)) return false; const order = auth.order; let changed = false; const existingCanonicalOrder = Array.isArray(order[OPENAI_PROVIDER_ID]) ? [...order[OPENAI_PROVIDER_ID]] : []; const legacyOrder = Array.isArray(order[LEGACY_OPENAI_CODEX_PROVIDER_ID]) ? order[LEGACY_OPENAI_CODEX_PROVIDER_ID] : []; const canonicalOrder = [...legacyOrder, ...existingCanonicalOrder]; const occupiedProfileIds = new Set(canonicalOrder.filter((entry) => typeof entry === "string" && !isLegacyOpenAICodexProfileId(entry))); for (const profileId of profileIdMap.values()) occupiedProfileIds.add(profileId); if (legacyOrder.length > 0) { delete order[LEGACY_OPENAI_CODEX_PROVIDER_ID]; changed = true; } const rewritten = canonicalOrder.map((entry) => { if (typeof entry !== "string") return entry; const mapped = profileIdMap.get(entry); if (mapped) return mapped; if (!isLegacyOpenAICodexProfileId(entry)) return entry; const canonicalProfileId = allocateOpenAIProfileId(entry, occupiedProfileIds); profileIdMap.set(entry, canonicalProfileId); return canonicalProfileId; }).filter((entry, index, entries) => typeof entry !== "string" || entries.indexOf(entry) === index); if (rewritten.length > 0) order[OPENAI_PROVIDER_ID] = rewritten; else if (OPENAI_PROVIDER_ID in order) delete order[OPENAI_PROVIDER_ID]; return changed || rewritten.some((entry, index) => entry !== canonicalOrder[index]); } function renameMappedProfileIdKeys(record, profileIdMap) { let changed = false; for (const [key, value] of Object.entries({ ...record })) { const nextKey = profileIdMap.get(key); if (!nextKey || nextKey === key) continue; delete record[key]; record[nextKey] = value; changed = true; } return changed; } function canonicalizeOpenAILastGood(record, profileIdMap) { let changed = false; const legacyValue = record[LEGACY_OPENAI_CODEX_PROVIDER_ID]; const canonicalValue = record[OPENAI_PROVIDER_ID]; if (legacyValue !== void 0) { delete record[LEGACY_OPENAI_CODEX_PROVIDER_ID]; changed = true; if (canonicalValue === void 0 && typeof legacyValue === "string") record[OPENAI_PROVIDER_ID] = profileIdMap.get(legacyValue) ?? legacyValue; } if (typeof record[OPENAI_PROVIDER_ID] === "string") { const mapped = profileIdMap.get(record[OPENAI_PROVIDER_ID]); if (mapped) { record[OPENAI_PROVIDER_ID] = mapped; changed = true; } } return changed; } /** * Canonicalizes config references from the legacy OpenAI Codex provider id to OpenAI. * * The optional map lets config and store repairs share deterministic profile ids when both surfaces * contain the same legacy profile. */ function maybeRepairOpenAICodexAuthConfig(cfg, options) { const config = structuredClone(cfg); const root = config; const auth = isRecord(root.auth) ? root.auth : void 0; const profileIdMap = new Map(options?.profileIdMap); let changed = false; if (isRecord(auth?.profiles)) { const rewrite = canonicalizeOpenAIProfileEntries(auth.profiles, { profileIdMap }); for (const [from, to] of rewrite.profileIdMap) profileIdMap.set(from, to); changed ||= rewrite.changed; } if (auth) { const orderChanged = canonicalizeOpenAIAuthOrder(auth, profileIdMap); changed ||= orderChanged; } if (profileIdMap.size > 0 && rewriteMappedAuthProfileRefs(config, profileIdMap)) changed = true; if (!changed) return { config, changes: [], warnings: [] }; return { config, changes: ["Migrated legacy OpenAI Codex auth profile config to the canonical OpenAI provider."], warnings: [] }; } function resolveOpenAICodexAuthStoreRepair(candidate, profileIdMap) { if (!fs.existsSync(candidate.authPath)) return null; const raw = loadJsonFile(candidate.authPath); if (!isRecord(raw) || !isRecord(raw.profiles)) return null; const rewrite = canonicalizeOpenAIProfileEntries(raw.profiles, { profileIdMap }); const orderChanged = canonicalizeOpenAIAuthOrder(raw, rewrite.profileIdMap); const usageChanged = isRecord(raw.usageStats) ? renameMappedProfileIdKeys(raw.usageStats, rewrite.profileIdMap) : false; const lastGoodChanged = isRecord(raw.lastGood) ? canonicalizeOpenAILastGood(raw.lastGood, rewrite.profileIdMap) : false; if (rewrite.profileIdMap.size > 0) replaceMappedProfileId(raw, rewrite.profileIdMap); const changed = rewrite.changed || orderChanged || usageChanged || lastGoodChanged; return changed ? { authPath: candidate.authPath, raw, profileIdMap: rewrite.profileIdMap, changed } : null; } /** Collects deterministic legacy-to-canonical OpenAI profile ids across all agent stores. */ function collectOpenAICodexAuthProfileStoreIdMap(params) { const env = params.env ?? process.env; const occupiedProfileIds = /* @__PURE__ */ new Set(); const legacyProfileIds = /* @__PURE__ */ new Set(); const profileIdMap = /* @__PURE__ */ new Map(); for (const candidate of listAuthProfileRepairCandidates(params.cfg, env)) { if (!fs.existsSync(candidate.authPath)) continue; const raw = loadJsonFile(candidate.authPath); if (!isRecord(raw) || !isRecord(raw.profiles)) continue; for (const profileId of Object.keys(raw.profiles)) if (isLegacyOpenAICodexProfileId(profileId)) legacyProfileIds.add(profileId); else occupiedProfileIds.add(profileId); } for (const profileId of [...legacyProfileIds].toSorted((a, b) => a.localeCompare(b))) profileIdMap.set(profileId, allocateOpenAIProfileId(profileId, occupiedProfileIds)); return profileIdMap; } function backupOpenAIProviderUnification(authPath, now) { const backupPath = `${authPath}.openai-provider-unification.${now()}.bak`; fs.copyFileSync(authPath, backupPath); return backupPath; } /** * Rewrites legacy OpenAI Codex auth profiles in JSON stores to the canonical OpenAI provider id. */ async function maybeRepairOpenAICodexAuthProfileStores(params) { const now = params.now ?? Date.now; const env = params.env ?? process.env; const profileIdMap = collectOpenAICodexAuthProfileStoreIdMap({ cfg: params.cfg, env }); const repairs = listAuthProfileRepairCandidates(params.cfg, env).map((candidate) => resolveOpenAICodexAuthStoreRepair(candidate, profileIdMap)).filter((entry) => entry !== null); const result = { detected: repairs.map((entry) => entry.authPath), changes: [], warnings: [] }; if (repairs.length === 0) return result; for (const entry of repairs) try { const backupPath = backupOpenAIProviderUnification(entry.authPath, now); fs.writeFileSync(entry.authPath, `${JSON.stringify(entry.raw, null, 2)}\n`); const movedCount = entry.profileIdMap.size; result.changes.push(`Migrated ${movedCount} OpenAI Codex auth profile(s) in ${shortenHomePath(entry.authPath)} to provider "openai" (backup: ${shortenHomePath(backupPath)}).`); } catch (err) { result.warnings.push(`Failed to migrate OpenAI Codex auth profiles in ${shortenHomePath(entry.authPath)}: ${String(err)}`); } clearRuntimeAuthProfileStoreSnapshots(); return result; } //#endregion export { maybeRepairOpenAICodexAuthConfig as a, maybeRepairLegacyFlatAuthProfileStores as i, maybeMigrateAuthProfileJsonStoresToSqlite as n, maybeRepairOpenAICodexAuthProfileStores as o, maybeRepairCanonicalApiKeyFieldAlias as r, collectOpenAICodexAuthProfileStoreIdMap as t };