openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
679 lines (678 loc) • 29.7 kB
JavaScript
import { s as normalizeOptionalLowercaseString } from "./string-coerce-mnp54Vah.js";
import { t as sanitizeForLog } from "./ansi-BI9w76Cm.js";
import { n as resolveOpenClawPackageRootSync } from "./openclaw-root-CNp1Ofdk.js";
import { y as resolveStateDir } from "./paths-mvMm5bYV.js";
import { p as normalizeUniqueStringEntries } from "./string-normalization-WNUDCpXX.js";
import { d as resolveConfigDir, p as resolveUserPath } from "./utils-CCC-BEJH.js";
import { i as GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA, r as normalizeChatChannelId } from "./ids-BVNPk-iM.js";
import { n as normalizeAccountId } from "./account-id-Df9e41E6.js";
import { E as validateConfigObjectWithPlugins } from "./io-Gi7-pyU-.js";
import { t as applyPluginAutoEnable } from "./plugin-auto-enable-pAtqig-B.js";
import { a as normalizeAnyChannelId } from "./registry-9YoS2BJP.js";
import { a as resolveChannelDmAllowFrom, s as setCanonicalDmAllowFrom } from "./dm-access-CTkWQ7VQ.js";
import { a as readChannelAllowFromStore } from "./pairing-store-CxANz_ON.js";
import { r as maybeRepairCodexRoutes } from "./codex-route-warnings-CS_FhR54.js";
import { a as collectChannelDoctorRepairMutations, s as createChannelDoctorEmptyAllowlistPolicyHooks } from "./channel-doctor-BpjhUDUf.js";
import { s as isUpdatePackageSwapInProgress } from "./update-phase-Br0EIrpk.js";
import { t as asObjectRecord } from "./object-BsiS9JXh.js";
import { t as repairMissingConfiguredPluginInstalls } from "./missing-configured-plugin-install-BMULB-0J.js";
import { a as maybeRepairOpenAICodexAuthConfig, n as maybeMigrateAuthProfileJsonStoresToSqlite, o as maybeRepairOpenAICodexAuthProfileStores, t as collectOpenAICodexAuthProfileStoreIdMap } from "./doctor-auth-flat-profiles-C0w6Ungg.js";
import { t as maybeRepairLegacyOAuthSidecarProfiles } from "./doctor-auth-oauth-sidecar-DszzdPpc.js";
import { t as applyDoctorConfigMutation } from "./config-mutation-state-DO_xkJud.js";
import { i as maybeRepairStaleManagedNpmBundledPlugins, t as maybeRepairManagedNpmOpenClawPeerLinks } from "./doctor-plugin-registry-nS1O3t0W.js";
import { t as collectActiveToolSchemaProjectionWarnings } from "./active-tool-schema-warnings-D1HoRepZ.js";
import { t as getDoctorChannelCapabilities } from "./channel-capabilities-D3l44zvp.js";
import { n as maybeRepairOpenPolicyAllowFrom, r as resolveAllowFromMode } from "./open-policy-allowfrom-BmoLQdgr.js";
import { n as hasAllowFromEntries, t as scanEmptyAllowlistPolicyWarnings } from "./empty-allowlist-scan-Cta_GOiN.js";
import { n as maybeRepairBundledPluginLoadPaths } from "./bundled-plugin-load-paths-DKHo99rC.js";
import { r as maybeRepairContextEngineHostCompatibility } from "./context-engine-host-compat-DdiO2HOE.js";
import { r as maybeRepairExecSafeBinProfiles } from "./exec-safe-bins-DUbnmQzR.js";
import { n as maybeRepairLegacyToolsBySenderKeys } from "./legacy-tools-by-sender-p3QJh6eV.js";
import { r as removeStalePluginRuntimeSymlinks } from "./plugin-runtime-symlinks-Kh4GEta_.js";
import { n as repairStaleOAuthProfileShadows } from "./stale-oauth-profile-shadows-CDADFNjP.js";
import { r as maybeRepairStalePluginConfig } from "./stale-plugin-config-CaUO-CRC.js";
import { n as maybeRepairStaleSubagentAllowlists } from "./stale-subagent-allowlist-Dl7-Xy7F.js";
import path from "node:path";
import fs from "node:fs/promises";
//#region src/commands/doctor/shared/allowfrom-fallback-migration.ts
const PSEUDO_CHANNEL_KEYS = new Set([
"defaults",
"modelByChannel",
"tools"
]);
const ACCOUNT_SCHEMA_WILDCARD = "*";
const CHANNEL_GROUP_ALLOW_FROM_PATH = ["groupAllowFrom"];
const ACCOUNT_GROUP_ALLOW_FROM_PATH = [
"accounts",
ACCOUNT_SCHEMA_WILDCARD,
"groupAllowFrom"
];
function isDisabled(record) {
return record.enabled === false;
}
function normalizeAllowFrom(raw) {
return normalizeUniqueStringEntries(Array.isArray(raw) ? raw : []);
}
function readGroupAllowFrom(record) {
return normalizeAllowFrom(record.groupAllowFrom);
}
function readDmAllowFrom(params) {
return normalizeAllowFrom(resolveChannelDmAllowFrom({
account: params.account,
parent: params.parent,
mode: getDoctorChannelCapabilities(params.channelName).dmAllowFromMode
}));
}
function readOwnDmAllowFrom(params) {
return normalizeAllowFrom(resolveChannelDmAllowFrom({
account: params.account,
mode: getDoctorChannelCapabilities(params.channelName).dmAllowFromMode
}));
}
function findGeneratedChannelConfigSchema(channelName) {
const normalizedChannelId = normalizeAnyChannelId(channelName);
return GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA.find((entry) => entry.channelId === channelName || entry.channelId === normalizedChannelId)?.schema;
}
function schemaAllowsConfigPath(schema, path) {
if (path.length === 0) return true;
const node = asObjectRecord(schema);
if (!node) return true;
const anyOf = Array.isArray(node.anyOf) ? node.anyOf : void 0;
if (anyOf) return anyOf.some((branch) => schemaAllowsConfigPath(branch, path));
const oneOf = Array.isArray(node.oneOf) ? node.oneOf : void 0;
if (oneOf) return oneOf.some((branch) => schemaAllowsConfigPath(branch, path));
const allOf = Array.isArray(node.allOf) ? node.allOf : void 0;
if (allOf) return allOf.every((branch) => schemaAllowsConfigPath(branch, path));
const [segment, ...rest] = path;
const properties = asObjectRecord(node.properties);
if (segment !== ACCOUNT_SCHEMA_WILDCARD && properties && Object.hasOwn(properties, segment)) return schemaAllowsConfigPath(properties[segment], rest);
const additionalProperties = node.additionalProperties;
if (additionalProperties === false) return false;
if (additionalProperties && typeof additionalProperties === "object") return schemaAllowsConfigPath(additionalProperties, rest);
return true;
}
function generatedSchemaAllowsGroupAllowFrom(channelName, path) {
const schema = findGeneratedChannelConfigSchema(channelName);
return !schema || schemaAllowsConfigPath(schema, path);
}
function migrateRecord(params) {
if (!params.canWriteGroupAllowFrom) return false;
if (readGroupAllowFrom(params.account).length > 0) return false;
if (params.parent && params.parentHadGroupAllowFrom) return false;
const ownAllowFrom = readOwnDmAllowFrom(params);
if (params.parent && ownAllowFrom.length === 0 && readGroupAllowFrom(params.parent).length > 0) return false;
const allowFrom = readDmAllowFrom(params);
if (allowFrom.length === 0) return false;
params.account.groupAllowFrom = allowFrom;
const noun = allowFrom.length === 1 ? "entry" : "entries";
params.changes.push(`${params.prefix}.groupAllowFrom: copied ${allowFrom.length} sender ${noun} from allowFrom for explicit group allowlist.`);
return true;
}
/** Copy legacy allowFrom entries into groupAllowFrom where channel metadata permits fallback. */
function maybeRepairGroupAllowFromFallback(cfg) {
if (!asObjectRecord(cfg.channels)) return {
config: cfg,
changes: []
};
const next = structuredClone(cfg);
const nextChannels = next.channels;
const changes = [];
for (const [channelName, channelConfig] of Object.entries(nextChannels)) {
if (PSEUDO_CHANNEL_KEYS.has(channelName) || !channelConfig || typeof channelConfig !== "object") continue;
if (isDisabled(channelConfig)) continue;
if (!getDoctorChannelCapabilities(channelName).groupAllowFromFallbackToAllowFrom) continue;
const hadGroupAllowFrom = readGroupAllowFrom(channelConfig).length > 0;
migrateRecord({
account: channelConfig,
canWriteGroupAllowFrom: generatedSchemaAllowsGroupAllowFrom(channelName, CHANNEL_GROUP_ALLOW_FROM_PATH),
channelName,
changes,
prefix: `channels.${channelName}`
});
const accounts = asObjectRecord(channelConfig.accounts);
if (!accounts) continue;
const canWriteAccountGroupAllowFrom = generatedSchemaAllowsGroupAllowFrom(channelName, ACCOUNT_GROUP_ALLOW_FROM_PATH);
for (const [accountId, accountConfig] of Object.entries(accounts)) {
const account = asObjectRecord(accountConfig);
if (!account || isDisabled(account)) continue;
migrateRecord({
account,
canWriteGroupAllowFrom: canWriteAccountGroupAllowFrom,
channelName,
changes,
parent: channelConfig,
parentHadGroupAllowFrom: hadGroupAllowFrom,
prefix: `channels.${channelName}.accounts.${accountId}`
});
}
}
if (changes.length === 0) return {
config: cfg,
changes: []
};
return {
config: next,
changes
};
}
//#endregion
//#region src/commands/doctor/shared/allowlist-policy-repair.ts
/** Restore missing allowFrom entries for allowlist DM policies from persisted pairing stores. */
async function maybeRepairAllowlistPolicyAllowFrom(cfg) {
const channels = cfg.channels;
if (!channels || typeof channels !== "object") return {
config: cfg,
changes: []
};
const next = structuredClone(cfg);
const changes = [];
const applyRecoveredAllowFrom = (params) => {
const count = params.allowFrom.length;
const noun = count === 1 ? "entry" : "entries";
setCanonicalDmAllowFrom({
entry: params.account,
mode: params.mode,
allowFrom: params.allowFrom,
pathPrefix: params.prefix,
changes,
reason: `restored ${count} sender ${noun} from pairing store (dmPolicy="allowlist").`
});
};
const recoverAllowFromForAccount = async (params) => {
const dmEntry = params.account.dm;
const dm = dmEntry && typeof dmEntry === "object" && !Array.isArray(dmEntry) ? dmEntry : void 0;
if ((params.account.dmPolicy ?? dm?.policy) !== "allowlist") return;
const topAllowFrom = params.account.allowFrom;
const nestedAllowFrom = dm?.allowFrom;
if (hasAllowFromEntries(topAllowFrom) || hasAllowFromEntries(nestedAllowFrom)) return;
const normalizedChannelId = normalizeOptionalLowercaseString(normalizeChatChannelId(params.channelName) ?? params.channelName);
if (!normalizedChannelId) return;
const normalizedAccountId = normalizeAccountId(params.accountId) || "default";
const recovered = normalizeUniqueStringEntries(await readChannelAllowFromStore(normalizedChannelId, process.env, normalizedAccountId).catch(() => []));
if (recovered.length === 0) return;
applyRecoveredAllowFrom({
account: params.account,
allowFrom: recovered,
mode: resolveAllowFromMode(params.channelName),
prefix: params.prefix
});
};
const nextChannels = next.channels;
for (const [channelName, channelConfig] of Object.entries(nextChannels)) {
if (!channelConfig || typeof channelConfig !== "object") continue;
if (channelConfig.enabled === false) continue;
await recoverAllowFromForAccount({
channelName,
account: channelConfig,
prefix: `channels.${channelName}`
});
const accounts = asObjectRecord(channelConfig.accounts);
if (!accounts) continue;
for (const [accountId, accountConfig] of Object.entries(accounts)) {
if (!accountConfig || typeof accountConfig !== "object") continue;
if (accountConfig.enabled === false) continue;
await recoverAllowFromForAccount({
channelName,
account: accountConfig,
accountId,
prefix: `channels.${channelName}.accounts.${accountId}`
});
}
}
if (changes.length === 0) return {
config: cfg,
changes: []
};
return {
config: next,
changes
};
}
//#endregion
//#region src/commands/doctor/shared/invalid-plugin-config.ts
const PLUGIN_CONFIG_ISSUE_RE = /^plugins\.entries\.([^.]+)\.config(?:\.|$)/;
function scanInvalidPluginConfig(cfg) {
const validation = validateConfigObjectWithPlugins(cfg);
if (validation.ok) return [];
const hits = [];
const seen = /* @__PURE__ */ new Set();
for (const issue of validation.issues) {
if (!issue.message.startsWith("invalid config:")) continue;
const pluginId = issue.path.match(PLUGIN_CONFIG_ISSUE_RE)?.[1];
if (!pluginId || seen.has(pluginId)) continue;
seen.add(pluginId);
hits.push({
pluginId,
pathLabel: `plugins.entries.${pluginId}.config`
});
}
return hits;
}
/** Disable plugin entries and clear config when plugin validation marks their config invalid. */
function maybeRepairInvalidPluginConfig(cfg) {
const hits = scanInvalidPluginConfig(cfg);
if (hits.length === 0) return {
config: cfg,
changes: []
};
const next = structuredClone(cfg);
const entries = asObjectRecord(next.plugins?.entries);
if (!entries) return {
config: cfg,
changes: []
};
const quarantined = [];
for (const hit of hits) {
const entry = asObjectRecord(entries[hit.pluginId]);
if (!entry) continue;
if ("config" in entry) delete entry.config;
entry.enabled = false;
quarantined.push(hit.pluginId);
}
if (quarantined.length === 0) return {
config: cfg,
changes: []
};
return {
config: next,
changes: [sanitizeForLog(`- plugins.entries: quarantined ${quarantined.length} invalid plugin config${quarantined.length === 1 ? "" : "s"} (${quarantined.join(", ")})`)]
};
}
//#endregion
//#region src/commands/doctor/shared/plugin-dependency-cleanup.ts
const LEGACY_DIRECT_CHILD_NAMES = new Set(["plugin-runtime-deps", "bundled-plugin-runtime-deps"]);
function uniqueSorted(values) {
return [...new Set([...values].filter((value) => typeof value === "string" && value.length > 0).map((value) => path.resolve(value)))].toSorted((left, right) => left.localeCompare(right));
}
function splitPathList(value) {
return value ? value.split(path.delimiter).map((entry) => entry.trim()).filter(Boolean) : [];
}
function hasParentPathSegment(value) {
return value.split(/[\\/]+/u).includes("..");
}
async function pathExists(targetPath) {
try {
await fs.lstat(targetPath);
return true;
} catch {
return false;
}
}
function isRuntimeDependencyMarkerName(name) {
return name === ".openclaw-runtime-deps.json" || name === ".openclaw-runtime-deps-stamp.json" || name.startsWith(".openclaw-runtime-deps-");
}
function isInstallStageDebrisName(name) {
return /^\.openclaw-install-stage(?:-.+)?$/u.test(name);
}
function isLegacyDependencyDebrisName(name) {
return isRuntimeDependencyMarkerName(name) || name === ".openclaw-pnpm-store" || name === ".openclaw-install-backups" || isInstallStageDebrisName(name);
}
function isExpectedLegacyCleanupTargetName(name) {
return name === "node_modules" || LEGACY_DIRECT_CHILD_NAMES.has(name) || isLegacyDependencyDebrisName(name);
}
async function isFile(targetPath) {
return (await fs.lstat(targetPath).catch(() => null))?.isFile() === true;
}
function isPathInsideRoot(candidate, root) {
const relativePath = path.relative(root, candidate);
return relativePath === "" || !relativePath.startsWith("..") && !path.isAbsolute(relativePath);
}
async function collectDirectChildren(root) {
return (await fs.readdir(root, { withFileTypes: true }).catch(() => [])).map((entry) => path.join(root, entry.name));
}
async function isDirectoryInCleanupRoot(candidate, cleanupRootRealPath) {
const stat = await fs.lstat(candidate).catch(() => null);
if (!stat?.isDirectory() && !stat?.isSymbolicLink()) return false;
const realPath = await fs.realpath(candidate).catch(() => null);
return realPath !== null && isPathInsideRoot(realPath, cleanupRootRealPath);
}
async function collectLegacyExtensionDebris(extensionsRoot, cleanupRootRealPath) {
if (!await isDirectoryInCleanupRoot(extensionsRoot, cleanupRootRealPath)) return [];
const pluginDirs = await fs.readdir(extensionsRoot, { withFileTypes: true }).catch(() => []);
const targets = [];
for (const entry of pluginDirs) {
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
const pluginRoot = path.join(extensionsRoot, entry.name);
if (!await isDirectoryInCleanupRoot(pluginRoot, cleanupRootRealPath)) continue;
const children = await collectDirectChildren(pluginRoot);
const hasRuntimeDepsMarker = children.some((childPath) => isRuntimeDependencyMarkerName(path.basename(childPath)));
for (const childPath of children) {
const basename = path.basename(childPath);
if (basename === "node_modules" && hasRuntimeDepsMarker) {
targets.push(childPath);
continue;
}
if (isLegacyDependencyDebrisName(basename)) targets.push(childPath);
}
}
return targets;
}
function collectCleanupRootPaths(env, packageRoot) {
const stateDirectoryRoots = splitPathList(env.STATE_DIRECTORY).map((entry) => resolveUserPath(entry, env));
return uniqueSorted([
resolveStateDir(env),
resolveConfigDir(env),
packageRoot,
...stateDirectoryRoots
]);
}
async function collectExistingCleanupRoots(cleanupRootPaths) {
const roots = [];
for (const rootPath of cleanupRootPaths) {
if (!(await fs.stat(rootPath).catch(() => null))?.isDirectory()) continue;
const realPath = await fs.realpath(rootPath).catch(() => null);
if (realPath === null) continue;
roots.push({ realPath });
}
return roots;
}
function collectExplicitStageTargets(env) {
return splitPathList(env.OPENCLAW_PLUGIN_STAGE_DIR).map((entry) => ({
kind: "explicit-stage",
path: resolveUserPath(entry, env),
rawPath: entry
}));
}
async function hasOpenClawRenameResidue(root) {
const nodeModulesRoot = path.join(root, "node_modules");
if (await isFile(path.join(nodeModulesRoot, ".openclaw-rename-tmp"))) return true;
const entries = await fs.readdir(nodeModulesRoot, { withFileTypes: true }).catch(() => []);
for (const entry of entries) {
if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
const entryPath = path.join(nodeModulesRoot, entry.name);
if (await isFile(path.join(entryPath, ".openclaw-rename-tmp"))) return true;
if (!entry.name.startsWith("@")) continue;
const scopedEntries = await fs.readdir(entryPath, { withFileTypes: true }).catch(() => []);
for (const scopedEntry of scopedEntries) {
if (!scopedEntry.isDirectory() || scopedEntry.isSymbolicLink()) continue;
if (await isFile(path.join(entryPath, scopedEntry.name, ".openclaw-rename-tmp"))) return true;
}
}
return false;
}
async function hasExplicitStageDebrisProof(root) {
if ((await collectDirectChildren(root)).some((childPath) => isRuntimeDependencyMarkerName(path.basename(childPath)))) return true;
return await hasOpenClawRenameResidue(root);
}
function filterLegacyStaleRootCandidates(targets, cleanupRootPaths) {
const safeTargets = [];
const warnings = [];
const seen = /* @__PURE__ */ new Set();
for (const target of targets) {
const targetPath = path.resolve(target.path);
if (seen.has(targetPath)) continue;
seen.add(targetPath);
if (target.kind === "explicit-stage") {
if (target.rawPath && hasParentPathSegment(target.rawPath)) {
warnings.push(`Skipped legacy plugin dependency state ${targetPath}: parent path segments are not allowed`);
continue;
}
safeTargets.push({
...target,
path: targetPath
});
continue;
}
if (!isExpectedLegacyCleanupTargetName(path.basename(targetPath))) {
warnings.push(`Skipped legacy plugin dependency state ${targetPath}: unexpected path name`);
continue;
}
if (!cleanupRootPaths.some((rootPath) => isPathInsideRoot(targetPath, rootPath))) {
warnings.push(`Skipped legacy plugin dependency state ${targetPath}: outside OpenClaw cleanup roots`);
continue;
}
safeTargets.push({
...target,
path: targetPath
});
}
return {
targets: safeTargets.toSorted((left, right) => left.path.localeCompare(right.path)),
warnings
};
}
async function resolveSafeRemovalTarget(target, cleanupRoots) {
const targetPath = path.resolve(target.path);
const stat = await fs.lstat(targetPath).catch(() => null);
if (target.kind === "explicit-stage" && stat?.isSymbolicLink()) return { warning: `Skipped legacy plugin dependency state ${targetPath}: symbolic link roots are not removed` };
const realPath = await fs.realpath(targetPath).catch(() => null);
if (realPath === null) return { warning: `Skipped legacy plugin dependency state ${targetPath}: could not resolve path` };
if (target.kind === "explicit-stage") {
if (!isInstallStageDebrisName(path.basename(targetPath)) && !await hasExplicitStageDebrisProof(targetPath)) return { warning: `Skipped legacy plugin dependency state ${targetPath}: unexpected path name` };
return { target: targetPath };
}
if (!cleanupRoots.some((root) => isPathInsideRoot(realPath, root.realPath))) return { warning: `Skipped legacy plugin dependency state ${targetPath}: resolved outside OpenClaw cleanup roots` };
return { target: targetPath };
}
async function prepareCleanupTargets(targets, cleanupRoots) {
const removalTargets = [];
const staleRoots = [];
const warnings = [];
for (const target of targets) {
if (!await pathExists(target.path)) continue;
const safeTarget = await resolveSafeRemovalTarget(target, cleanupRoots);
if ("warning" in safeTarget) {
warnings.push(safeTarget.warning);
continue;
}
removalTargets.push(safeTarget.target);
staleRoots.push(safeTarget.target);
}
return {
removalTargets: uniqueSorted(removalTargets),
staleRoots: uniqueSorted(staleRoots),
warnings
};
}
async function collectLegacyPluginDependencyTargetEntries(env = process.env, options = {}) {
const packageRoot = options.packageRoot ?? resolveOpenClawPackageRootSync({
argv1: process.argv[1],
moduleUrl: import.meta.url,
cwd: process.cwd()
});
const roots = uniqueSorted([
resolveStateDir(env),
resolveConfigDir(env),
packageRoot
]);
const stateDirectoryRoots = splitPathList(env.STATE_DIRECTORY).map((entry) => ({
kind: "legacy",
path: path.join(resolveUserPath(entry, env), "plugin-runtime-deps")
}));
const targets = [
...collectExplicitStageTargets(env),
...stateDirectoryRoots,
...roots.flatMap((root) => [...[...LEGACY_DIRECT_CHILD_NAMES].map((name) => ({
kind: "legacy",
path: path.join(root, name)
})), {
kind: "legacy",
path: path.join(root, ".local", "bundled-plugin-runtime-deps")
}])
];
for (const root of roots) {
const rootRealPath = await fs.realpath(root).catch(() => null);
if (rootRealPath === null) continue;
targets.push(...(await collectLegacyExtensionDebris(path.join(root, "extensions"), rootRealPath)).map((targetPath) => ({
kind: "legacy",
path: targetPath
})));
targets.push(...(await collectLegacyExtensionDebris(path.join(root, "dist", "extensions"), rootRealPath)).map((targetPath) => ({
kind: "legacy",
path: targetPath
})));
}
return targets.toSorted((left, right) => left.path.localeCompare(right.path));
}
/** Remove legacy plugin dependency state under trusted OpenClaw cleanup roots. */
async function cleanupLegacyPluginDependencyState(params) {
const env = params.env ?? process.env;
const changes = [];
const warnings = [];
const packageRoot = params.packageRoot ?? resolveOpenClawPackageRootSync({
argv1: process.argv[1],
moduleUrl: import.meta.url,
cwd: process.cwd()
});
const targets = await collectLegacyPluginDependencyTargetEntries(env, { packageRoot });
const cleanupRootPaths = collectCleanupRootPaths(env, packageRoot);
const cleanupRoots = await collectExistingCleanupRoots(cleanupRootPaths);
const staleRootCandidates = filterLegacyStaleRootCandidates(targets, cleanupRootPaths);
warnings.push(...staleRootCandidates.warnings);
const preparedTargets = await prepareCleanupTargets(staleRootCandidates.targets, cleanupRoots);
warnings.push(...preparedTargets.warnings);
const staleSymlinks = await removeStalePluginRuntimeSymlinks(packageRoot, { staleRoots: preparedTargets.staleRoots });
changes.push(...staleSymlinks.changes);
warnings.push(...staleSymlinks.warnings);
for (const target of preparedTargets.removalTargets) try {
await fs.rm(target, {
recursive: true,
force: true
});
changes.push(`Removed legacy plugin dependency state: ${target}`);
} catch (error) {
warnings.push(`Failed to remove legacy plugin dependency state ${target}: ${String(error)}`);
}
return {
changes,
warnings
};
}
//#endregion
//#region src/commands/doctor/repair-sequencing.ts
/** Run doctor auto-repairs in dependency order and collect sanitized user notes. */
async function runDoctorRepairSequence(params) {
let state = params.state;
const changeNotes = [];
const warningNotes = [];
const env = params.env ?? process.env;
const sanitizeLines = (lines) => lines.map((line) => sanitizeForLog(line)).join("\n");
const applyMutation = (mutation) => {
if (mutation.changes.length > 0) {
changeNotes.push(sanitizeLines(mutation.changes));
state = applyDoctorConfigMutation({
state,
mutation,
shouldRepair: true
});
}
if (mutation.warnings && mutation.warnings.length > 0) warningNotes.push(sanitizeLines(mutation.warnings));
};
for (const mutation of await collectChannelDoctorRepairMutations({
cfg: state.candidate,
doctorFixCommand: params.doctorFixCommand,
env
})) applyMutation(mutation);
applyMutation(maybeRepairBundledPluginLoadPaths(state.candidate, env));
maybeRepairStaleManagedNpmBundledPlugins({
config: state.candidate,
env,
prompter: { shouldRepair: true }
});
await maybeRepairManagedNpmOpenClawPeerLinks({
config: state.candidate,
env,
prompter: { shouldRepair: true }
});
const codexRouteRepair = maybeRepairCodexRoutes({
cfg: state.candidate,
env,
shouldRepair: true
});
applyMutation({
config: codexRouteRepair.cfg,
changes: codexRouteRepair.changes,
warnings: codexRouteRepair.warnings
});
applyMutation(maybeRepairOpenAICodexAuthConfig(state.candidate, { profileIdMap: collectOpenAICodexAuthProfileStoreIdMap({
cfg: state.candidate,
env
}) }));
applyMutation(await maybeRepairContextEngineHostCompatibility({
cfg: state.candidate,
doctorFixCommand: params.doctorFixCommand,
env
}));
const missingConfiguredPluginInstallRepair = await repairMissingConfiguredPluginInstalls({
cfg: state.candidate,
env
});
if (missingConfiguredPluginInstallRepair.changes.length > 0) {
changeNotes.push(sanitizeLines(missingConfiguredPluginInstallRepair.changes));
applyMutation(applyPluginAutoEnable({
config: state.candidate,
env
}));
}
if (missingConfiguredPluginInstallRepair.warnings.length > 0) warningNotes.push(sanitizeLines(missingConfiguredPluginInstallRepair.warnings));
const failedPluginIds = missingConfiguredPluginInstallRepair.failedPluginIds ?? [];
const hasUnscopedInstallRepairWarnings = missingConfiguredPluginInstallRepair.warnings.length > 0 && failedPluginIds.length === 0;
if (!isUpdatePackageSwapInProgress(env) && !hasUnscopedInstallRepairWarnings) applyMutation(maybeRepairStalePluginConfig(state.candidate, env, { preservePluginIds: failedPluginIds }));
applyMutation(maybeRepairInvalidPluginConfig(state.candidate));
applyMutation(await maybeRepairAllowlistPolicyAllowFrom(state.candidate));
applyMutation(maybeRepairOpenPolicyAllowFrom(state.candidate));
applyMutation(maybeRepairGroupAllowFromFallback(state.candidate));
applyMutation(maybeRepairStaleSubagentAllowlists(state.candidate));
const emptyAllowlistWarnings = scanEmptyAllowlistPolicyWarnings(state.candidate, {
doctorFixCommand: params.doctorFixCommand,
...createChannelDoctorEmptyAllowlistPolicyHooks({
cfg: state.candidate,
env
})
});
if (emptyAllowlistWarnings.length > 0) warningNotes.push(sanitizeLines(emptyAllowlistWarnings));
applyMutation(maybeRepairLegacyToolsBySenderKeys(state.candidate));
applyMutation(maybeRepairExecSafeBinProfiles(state.candidate));
const pluginDependencyCleanup = await cleanupLegacyPluginDependencyState({ env });
if (pluginDependencyCleanup.changes.length > 0) changeNotes.push(sanitizeLines(pluginDependencyCleanup.changes));
if (pluginDependencyCleanup.warnings.length > 0) warningNotes.push(sanitizeLines(pluginDependencyCleanup.warnings));
const legacyOAuthSidecarRepair = await maybeRepairLegacyOAuthSidecarProfiles({
cfg: state.candidate,
prompter: { confirmAutoFix: async () => true },
emitNotes: false,
env
});
if (legacyOAuthSidecarRepair.changes.length > 0) changeNotes.push(sanitizeLines(legacyOAuthSidecarRepair.changes));
if (legacyOAuthSidecarRepair.warnings.length > 0) warningNotes.push(sanitizeLines(legacyOAuthSidecarRepair.warnings));
const openAIAuthProviderRepair = await maybeRepairOpenAICodexAuthProfileStores({
cfg: state.candidate,
env
});
if (openAIAuthProviderRepair.changes.length > 0) changeNotes.push(sanitizeLines(openAIAuthProviderRepair.changes));
if (openAIAuthProviderRepair.warnings.length > 0) warningNotes.push(sanitizeLines(openAIAuthProviderRepair.warnings));
const staleOAuthShadowRepair = await repairStaleOAuthProfileShadows({
cfg: state.candidate,
env
});
if (staleOAuthShadowRepair.changes.length > 0) changeNotes.push(sanitizeLines(staleOAuthShadowRepair.changes));
if (staleOAuthShadowRepair.warnings.length > 0) warningNotes.push(sanitizeLines(staleOAuthShadowRepair.warnings));
const authProfileSqliteMigration = await maybeMigrateAuthProfileJsonStoresToSqlite({
cfg: state.candidate,
prompter: { confirmAutoFix: async () => true },
env
});
if (authProfileSqliteMigration.configChanged) state = applyDoctorConfigMutation({
state,
mutation: {
config: state.candidate,
changes: ["Auth profile SQLite migration updated auth.profiles."]
},
shouldRepair: true
});
if (authProfileSqliteMigration.changes.length > 0) changeNotes.push(sanitizeLines(authProfileSqliteMigration.changes));
if (authProfileSqliteMigration.warnings.length > 0) warningNotes.push(sanitizeLines(authProfileSqliteMigration.warnings));
const authProfilesRepaired = legacyOAuthSidecarRepair.changes.length > 0 || openAIAuthProviderRepair.changes.length > 0 || staleOAuthShadowRepair.changes.length > 0 || authProfileSqliteMigration.changes.length > 0;
const activeToolSchemaWarnings = collectActiveToolSchemaProjectionWarnings({
cfg: state.candidate,
env
});
if (activeToolSchemaWarnings.length > 0) warningNotes.push(sanitizeLines(activeToolSchemaWarnings));
return {
state,
changeNotes,
warningNotes,
authProfilesRepaired
};
}
//#endregion
export { runDoctorRepairSequence };