openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
422 lines (421 loc) • 23.5 kB
JavaScript
import { C as resolveIsNixMode, S as resolveIsConfigReadOnly } from "./paths-DGIwWbpu.mjs";
import { t as formatErrorMessage } from "./errors-O33D01XW.mjs";
import { i as resolveUpdateRehearsalRoot } from "./update-rehearsal-paths-DTVdt0cq.mjs";
import { m as shouldSkipLegacyUpdateDoctorConfigWrite } from "./update-phase-BClSJ17n.mjs";
import { g as recordUpdateDoctorConfigMigration, p as getUpdateDoctorConfigWriteAuthority, v as recordUpdateDoctorConfigWriteRefusal, y as runUpdateDoctorIncludeWrite } from "./update-doctor-result-b1ffujJO.mjs";
import { t as ConfigWritePostCommitError } from "./io.write-errors-DuSgsySH.mjs";
import { n as recordUpdateModelRetirement } from "./update-deferred-model-retirement-CXPEpk-c.mjs";
import { i as resolveLegacyParentVersionOverride, n as resolveDoctorMode, t as isUpdateDoctorRun } from "./doctor-health-contribution-utils-qU6dG96F.mjs";
import fs from "node:fs";
import path from "node:path";
//#region src/flows/doctor-health-contribution-runners.config.ts
/** Removes queued retired profiles after any config references have been durably repaired. */
async function runRetiredAuthProfileCleanup(ctx) {
if (ctx.externalConfigRepairsPending) return;
const retiredAuthProfileCleanupPlans = ctx.configResult.retiredAuthProfileCleanupPlans;
if (!retiredAuthProfileCleanupPlans?.length) return;
const { removeAuthProfilesAcrossOwnerStores } = await import("./auth-profiles-BoWjlBsR.mjs");
for (const plan of retiredAuthProfileCleanupPlans) if (!await removeAuthProfilesAcrossOwnerStores({
...plan,
cfg: ctx.cfg
})) throw new Error(`Failed to remove retired auth profile "${plan.profileIds.join(", ")}".`);
delete ctx.configResult.retiredAuthProfileCleanupPlans;
}
/** Returns false when persistence was refused or skipped, without authorizing dependent work. */
async function runWriteConfigHealth(ctx, options = {}) {
if (ctx.configWriteError) throw ctx.configWriteError;
if (ctx.configWriteRefusal) return false;
const { applyWizardMetadata } = await import("./onboard-helpers-D3youbzG.mjs");
const { ConfigMutationConflictError, readConfigFileSnapshot, transformConfigFile } = await import("./config/config.js");
const { collectChangedConfigPaths } = await import("./include-write-boundary-DLdH1yKO.mjs");
const { hashConfigRaw } = await import("./io.read-helpers-DzqIh7Xd.mjs");
const { resolveConfigIncludeWriteBoundary } = await import("./mutate-CzUwEAEO.mjs");
const { isDeepStrictEqual } = await import("node:util");
const { getDeferredPluginMigrationConfigFacts, preserveDeferredPluginMigrationConfig } = await import("./deferred-plugin-migration-config-DJP7yWpQ.mjs");
const { createSubsystemLogger } = await import("./subsystem-CJxVDa1V.mjs");
const { recordDoctorHealthWarnings } = await import("./doctor-health-contribution-BjwyZZJH.mjs");
const { logConfigUpdated } = await import("./logging-n-0MDc5D.mjs");
const { shortenHomePath } = await import("./utils-CZEHCb0v.mjs");
const configResultWritePending = ctx.configResult.shouldWriteConfig === true && ctx.configResultWriteCommitted !== true;
const shouldWriteConfig = configResultWritePending || JSON.stringify(ctx.cfg) !== JSON.stringify(ctx.cfgForPersistence);
if (resolveIsConfigReadOnly(ctx.env ?? process.env)) {
if (shouldWriteConfig) {
const { reportDoctorExternalConfigRepairs } = await import("./doctor-external-config-CnhCObFy.mjs");
await reportDoctorExternalConfigRepairs(ctx);
return false;
}
if (ctx.externalConfigRepairsPending) return false;
}
if (shouldWriteConfig) {
const updateDoctorRun = isUpdateDoctorRun(ctx.env ?? process.env);
const { restoreDoctorConfigEnvRefs } = await import("./config-flow-steps-Bcjuk29E.mjs");
const { prepareDoctorConfigWriteStage } = await import("./roster-include-write-DnBU5qqv.mjs");
let stageSnapshot = configResultWritePending ? await readConfigFileSnapshot({
observe: false,
skipPluginValidation: true
}) : void 0;
let stage = stageSnapshot ? prepareDoctorConfigWriteStage({
snapshot: stageSnapshot,
nextConfig: restoreDoctorConfigEnvRefs(ctx.cfg, ctx.configResult.referenceSource, ctx.configResult.explicitSetPaths),
persistCanonicalAgentRoster: ctx.configResult.persistCanonicalAgentRoster,
installedPluginIdRecovery: ctx.configResult.referenceSource?.installedPluginIdRecovery,
explicitSetPaths: ctx.configResult.explicitSetPaths
}) : void 0;
if (stage) ctx.configResult.skipWizardMetadataForIncludeWrite = true;
if (ctx.configResult.skipWizardMetadataForIncludeWrite !== true && !resolveUpdateRehearsalRoot(ctx.env ?? process.env)) ctx.cfg = applyWizardMetadata(ctx.cfg, {
command: "doctor",
mode: resolveDoctorMode(ctx.cfg)
});
if (shouldSkipLegacyUpdateDoctorConfigWrite(ctx.env ?? process.env)) {
ctx.runtime.log("Skipping doctor config write during legacy update handoff.");
return false;
}
const legacyParentVersionOverride = resolveLegacyParentVersionOverride(ctx).lastTouchedVersionOverride;
const { findRetiredConfigUpgradeRequirement } = await import("./retired-config-formats-CUx9S2jN.mjs");
const { assertInstalledPluginIdRecoveryCurrent } = await import("./installed-plugin-id-recovery-l7pcSmXO.mjs");
const installedPluginIdRecovery = ctx.configResult.referenceSource?.installedPluginIdRecovery;
let committed;
let stagedWriteCommitted = false;
try {
while (stage && stageSnapshot) {
const currentStage = stage;
const stageContext = {
...ctx,
cfg: currentStage.config,
cfgForPersistence: stageSnapshot.sourceConfig,
configResultWriteCommitted: false,
configResult: {
cfg: currentStage.config,
shouldWriteConfig: true,
confirmedConfigSource: ctx.configResult.confirmedConfigSource,
referenceSource: ctx.configResult.referenceSource,
persistCanonicalAgentRoster: currentStage.persistCanonicalAgentRoster,
skipWizardMetadataForIncludeWrite: true,
skipPluginValidationOnWrite: currentStage.persistCanonicalAgentRoster || ctx.configResult.skipPluginValidationOnWrite,
preservedLegacyRootKeys: ctx.configResult.preservedLegacyRootKeys,
explicitSetPaths: ctx.configResult.explicitSetPaths?.filter(([key]) => !currentStage.persistCanonicalAgentRoster || key === "agents")
}
};
try {
if (!await runWriteConfigHealth(stageContext, { runPostWriteRepairs: false })) {
ctx.configWriteRefusal = stageContext.configWriteRefusal;
return false;
}
} finally {
if (stageContext.configWriteError) ctx.configWriteError = stageContext.configWriteError;
if (stageContext.configResultWriteCommitted) {
ctx.configResult.confirmedConfigSource = stageContext.configResult.confirmedConfigSource;
ctx.cfgForPersistence = stageContext.cfgForPersistence;
if (currentStage.persistCanonicalAgentRoster) delete ctx.configResult.persistCanonicalAgentRoster;
stagedWriteCommitted = true;
}
}
const message = "Saved a config migration stage; remaining repairs are still pending.";
recordUpdateDoctorConfigMigration(message);
ctx.runtime.log(message);
const savedStage = await readConfigFileSnapshot({
observe: false,
skipPluginValidation: true
});
if (savedStage.path !== ctx.configResult.confirmedConfigSource?.path || savedStage.hash !== ctx.configResult.confirmedConfigSource?.hash) throw new ConfigMutationConflictError("config changed after Doctor saved a migration stage", { retryable: false });
const { cloneConfigWithResolutionFacts } = await import("./resolution-facts-CEwQo43-.mjs");
const { getConfigValueAtPath, setConfigValueAtPath } = await import("./config-paths-IuF23hFI.mjs");
const pending = ctx.cfg;
ctx.cfg = cloneConfigWithResolutionFacts(savedStage.sourceConfig);
for (const field of currentStage.remainingPaths) setConfigValueAtPath(ctx.cfg, field, getConfigValueAtPath(pending, field));
stageSnapshot = savedStage;
stage = prepareDoctorConfigWriteStage({
snapshot: savedStage,
nextConfig: ctx.cfg,
persistCanonicalAgentRoster: ctx.configResult.persistCanonicalAgentRoster,
installedPluginIdRecovery,
explicitSetPaths: ctx.configResult.explicitSetPaths
});
}
const confirmedConfigSource = ctx.configResult.confirmedConfigSource;
if (!confirmedConfigSource?.hash) throw new ConfigMutationConflictError("Doctor config write has no source revision", { retryable: false });
const { path, hash } = confirmedConfigSource;
const nextConfig = restoreDoctorConfigEnvRefs(ctx.cfg, ctx.configResult.referenceSource, ctx.configResult.explicitSetPaths);
const authority = getUpdateDoctorConfigWriteAuthority(ctx.configPath);
const includeSnapshot = authority ? await readConfigFileSnapshot({
skipPluginValidation: updateDoctorRun,
observe: false
}) : void 0;
const includeBoundary = includeSnapshot && resolveConfigIncludeWriteBoundary({
snapshot: includeSnapshot,
nextConfig,
persistCanonicalAgentRoster: configResultWritePending ? ctx.configResult.persistCanonicalAgentRoster : void 0,
explicitSetPaths: ctx.configResult.explicitSetPaths
});
const includeWrite = includeBoundary ? includeSnapshot : void 0;
let recoveryConfig = ctx.cfg;
const persistConfig = (assertOwned) => transformConfigFile({
baseHash: hash,
transform: async (_current, { snapshot }) => {
assertOwned?.();
authority?.assertCurrent();
const retired = findRetiredConfigUpgradeRequirement(snapshot.sourceConfigBeforeMigrations ?? snapshot.sourceConfig);
if (retired) throw new ConfigMutationConflictError(`${retired.message} ${retired.nextAction}`);
recoveryConfig = snapshot.sourceConfig;
await assertInstalledPluginIdRecoveryCurrent(recoveryConfig, installedPluginIdRecovery, ctx.env ?? process.env);
authority?.assertCurrent();
assertOwned?.();
if (includeBoundary) {
const currentBoundary = resolveConfigIncludeWriteBoundary({
snapshot,
nextConfig,
persistCanonicalAgentRoster: configResultWritePending ? ctx.configResult.persistCanonicalAgentRoster : void 0,
explicitSetPaths: ctx.configResult.explicitSetPaths
});
if (!isDeepStrictEqual(currentBoundary, includeBoundary)) throw new ConfigMutationConflictError("included config changed after Doctor prepared its repairs", { retryable: false });
}
const { repairLegacyCronOwnersBeforeConfigWrite } = await import("./legacy-owner-CsasGkwt.mjs");
const cronOwnerChanges = await repairLegacyCronOwnersBeforeConfigWrite({
snapshot,
nextConfig,
env: ctx.env ?? process.env,
assertCurrent: () => {
authority?.assertCurrent();
assertOwned?.();
}
});
for (const change of cronOwnerChanges) ctx.runtime.log(change);
return { nextConfig };
},
afterWrite: { mode: "auto" },
writeOptions: {
assertCurrent: () => {
authority?.assertCurrent();
assertOwned?.();
},
...installedPluginIdRecovery?.size ? { beforeCommit: async () => {
authority?.assertCurrent();
assertOwned?.();
await assertInstalledPluginIdRecoveryCurrent(recoveryConfig, installedPluginIdRecovery, ctx.env ?? process.env);
authority?.assertCurrent();
assertOwned?.();
} } : {},
expectedConfigPath: path,
auditOrigin: "doctor",
allowConfigSizeDrop: ctx.configResult.shouldWriteConfig === true || updateDoctorRun,
skipPluginValidation: ctx.configResult.skipPluginValidationOnWrite === true || updateDoctorRun,
...ctx.configResult.explicitSetPaths ? { explicitSetPaths: ctx.configResult.explicitSetPaths } : {},
persistCanonicalAgentRoster: configResultWritePending ? ctx.configResult.persistCanonicalAgentRoster : void 0,
preservedLegacyRootKeys: ctx.configResult.preservedLegacyRootKeys,
...legacyParentVersionOverride ? { lastTouchedVersionOverride: legacyParentVersionOverride } : {}
}
});
const writeConfig = async () => {
if (!installedPluginIdRecovery?.size) return await persistConfig();
const { withPluginLifecycleLease } = await import("./plugin-lifecycle-lease-D_2q8UjY.mjs");
return await withPluginLifecycleLease({
env: ctx.env ?? process.env,
assertCurrent: () => authority?.assertCurrent()
}, (lease) => persistConfig(() => lease.assertOwned()));
};
if (includeWrite) {
const keys = [...new Set(collectChangedConfigPaths(includeWrite.sourceConfig, ctx.cfg).paths.flatMap(([key]) => key === void 0 ? [] : [key]))].toSorted();
committed = await runUpdateDoctorIncludeWrite(includeWrite.path, hashConfigRaw(includeWrite.raw), async () => {
const warning = `Doctor include-owned keys ${keys.join(", ")}: promotion unavailable for include-owned configuration.`;
recordDoctorHealthWarnings(ctx, [], [warning]);
createSubsystemLogger("update").warn(warning);
ctx.runtime.log(warning);
return await writeConfig();
});
} else committed = await writeConfig();
} catch (error) {
if (error instanceof ConfigWritePostCommitError) {
ctx.configWriteError = error;
if (error.publication === "partial") delete ctx.configResult.confirmedConfigSource;
throw error;
}
recordUpdateDoctorConfigWriteRefusal({
reason: "config-write-refused",
message: formatErrorMessage(error),
keys: []
});
if (error instanceof ConfigMutationConflictError) {
const { note } = await import("./terminal-core/note.js");
note(["The config changed after Doctor prepared these repairs.", stagedWriteCommitted ? "Earlier config fixes were saved; the remaining fixes were not written. Rerun \"openclaw doctor\" to review the current config." : "These config fixes were not written. Rerun \"openclaw doctor\" to review repairs for the current config."].join("\n"), "Doctor warnings");
ctx.configWriteRefusal = "config-conflict";
return false;
}
const { isConfigIncludeOwnershipError, isConfigValidationFailedError } = await import("./io.write-errors-B1yzBlh3.mjs");
const unpersistedLine = ctx.configResultWriteCommitted === true || stagedWriteCommitted ? "Earlier config fixes were already saved; the remaining changes were not written." : "No config changes were written.";
if (isConfigIncludeOwnershipError(error)) {
const { note } = await import("./terminal-core/note.js");
const targets = error.includeTargets ?? [];
const includedFile = targets.length === 0 ? "its included file" : `the included ${targets.length === 1 ? "file" : "files"} ${targets.join(", ")}`;
note([`Doctor could not apply config fixes: ${error.message}`, `${unpersistedLine} Repair ${error.ownedConfigPath} in ${includedFile} by hand, then rerun "openclaw doctor --fix" for the remaining changes.`].join("\n"), "Doctor warnings");
ctx.configWriteRefusal = "include-ownership";
return false;
}
if (isConfigValidationFailedError(error)) {
const { note } = await import("./terminal-core/note.js");
const { formatConfigIssueLines } = await import("./issue-format-Bj4Iwq7G.mjs");
note([
"Doctor could not apply config fixes: the repaired config still fails validation.",
...Array.isArray(error.issues) ? formatConfigIssueLines(error.issues, "-", { normalizeRoot: true }) : [error.message],
`${unpersistedLine} Fix the value(s) above in ${shortenHomePath(ctx.configPath)} by hand, then rerun "openclaw doctor --fix".`
].join("\n"), "Doctor warnings");
ctx.configWriteRefusal = "validation";
return false;
}
const { isCronOwnerWriteRefusalError } = await import("./io.cron-owner-refusal-CsXeXB8q.mjs");
if (!isCronOwnerWriteRefusalError(error)) throw error;
const { note } = await import("./terminal-core/note.js");
note([
error.message,
stagedWriteCommitted ? "Earlier config fixes were saved; the remaining config repairs were not written." : "Doctor left the config unchanged, preserving any retained legacy owner for a later repair.",
"Resolve the reported Gateway or cron-store condition, then rerun \"openclaw doctor --fix\"."
].join("\n"), "Doctor warnings");
ctx.configWriteRefusal = "cron-owner-safety";
return false;
}
ctx.configResult.confirmedConfigSource = {
path: committed.path,
hash: committed.persistedHash
};
const pendingChangePanels = ctx.configResult.pendingChangePanels;
if (pendingChangePanels?.length) {
const { note } = await import("./terminal-core/note.js");
for (const panel of pendingChangePanels) {
note(panel, "Doctor changes");
for (const message of panel.split("\n")) recordUpdateDoctorConfigMigration(message);
}
delete ctx.configResult.pendingChangePanels;
}
ctx.cfgForPersistence = structuredClone(preserveDeferredPluginMigrationConfig({
sourceConfig: committed.nextConfig,
nextConfig: ctx.cfg,
pending: getDeferredPluginMigrationConfigFacts(committed.nextConfig) ?? []
}));
if (ctx.configResult.shouldWriteConfig === true) ctx.configResultWriteCommitted = true;
logConfigUpdated(ctx.runtime);
const preUpdateSnapshotPath = `${ctx.configPath}.pre-update`;
if (updateDoctorRun && fs.existsSync(preUpdateSnapshotPath)) ctx.runtime.log(`Update changed config; pre-update backup: ${shortenHomePath(preUpdateSnapshotPath)}`);
}
if (ctx.configResult.modelRetirementRepairRan === true) {
recordUpdateModelRetirement("completed", ctx.env ?? process.env);
delete ctx.configResult.modelRetirementRepairRan;
}
const billingWarnings = ctx.configResult.modelBillingRouteWarnings;
if (billingWarnings?.length) {
const { note } = await import("./terminal-core/note.js");
const log = createSubsystemLogger("doctor");
note(billingWarnings.join("\n"), "Billing route changes");
for (const warning of billingWarnings) log.warn(warning);
recordDoctorHealthWarnings(ctx, [], billingWarnings);
delete ctx.configResult.modelBillingRouteWarnings;
}
if (options.runPostWriteRepairs === false) return true;
await runRetiredAuthProfileCleanup(ctx);
if (ctx.configResult.retiredPhoneControlStateCleanupPending === true) {
const { finalizeRetiredPhoneControlCleanup } = await import("./doctor-retired-phone-control-B7TieL3q.mjs");
const { note } = await import("./terminal-core/note.js");
const cleanup = await finalizeRetiredPhoneControlCleanup({ env: ctx.env ?? process.env });
if (cleanup.changes.length > 0) note(cleanup.changes.join("\n"), "Doctor changes");
if (cleanup.warnings.length > 0) note(cleanup.warnings.join("\n"), "Doctor warnings");
}
if (!ctx.prompter.shouldRepair && !ctx.configResult.openAICodexAuthProfileIdMap?.size && ctx.configResult.shouldRepairCronCodexModelRefsAfterConfigWrite !== true || ctx.postConfigWriteRepairsCommitted === true) return true;
const { repairCronCodexModelRefsAfterConfigWrite } = await import("./legacy-repair-B3Id5yeg.mjs");
const result = await repairCronCodexModelRefsAfterConfigWrite({
cfg: ctx.cfg,
migrateCodexModelRefs: ctx.prompter.shouldRepair || ctx.configResult.shouldRepairCronCodexModelRefsAfterConfigWrite === true,
...ctx.configResult.retiredModelRefConfig ? { retiredModelRefConfig: ctx.configResult.retiredModelRefConfig } : {},
repairRetiredModelRefs: ctx.prompter.shouldRepair,
authProfileIdMap: ctx.configResult.openAICodexAuthProfileIdMap,
...ctx.configResult.blockedCodexModelIdentities?.length ? { blockedModelIdentities: new Set(ctx.configResult.blockedCodexModelIdentities) } : {}
});
ctx.postConfigWriteRepairsCommitted = true;
const { note } = await import("./terminal-core/note.js");
if (result.changes.length > 0) note(result.changes.join("\n"), "Doctor changes");
if (result.warnings.length > 0) note(result.warnings.join("\n"), "Doctor warnings");
return true;
}
/** Commits the finalized config-flow candidate before fallible health diagnostics start. */
async function runInitialConfigWriteHealth(ctx) {
if (ctx.configResult.shouldWriteConfig !== true && !ctx.configResult.modelBillingRouteWarnings?.length && ctx.configResult.modelRetirementRepairRan !== true) return;
await runWriteConfigHealth(ctx, { runPostWriteRepairs: false });
}
async function collectWriteConfigHealthFindings(ctx) {
const findings = [];
const configPath = ctx.configPath;
const isNixMode = resolveIsNixMode(process.env);
if (resolveIsConfigReadOnly(process.env)) findings.push({
checkId: "core/doctor/write-config",
severity: "warning",
message: isNixMode ? "Doctor config writes are disabled because OpenClaw is running in Nix mode." : "Doctor config writes are disabled because config is externally managed.",
...configPath ? { path: configPath } : {},
requirement: "mutable-config-write-path",
fixHint: isNixMode ? "Edit the Nix source for this install and rebuild; do not run doctor --fix against this config file." : "Apply reported config edits in your external deployment source and redeploy; doctor --fix still repairs writable runtime state."
});
if (!configPath) return findings;
const configDirectory = path.dirname(configPath);
const configPathExists = fs.existsSync(configPath);
const existingParent = configPathExists ? configDirectory : findNearestExistingParent(configDirectory);
if (!isDirectoryPath(existingParent)) {
findings.push({
checkId: "core/doctor/write-config",
severity: "warning",
message: "Doctor cannot create the config directory because a path component is a file.",
path: existingParent,
target: configDirectory,
requirement: "config-directory-path",
fixHint: "Move the file blocking the config directory path before running doctor --fix."
});
return findings;
}
try {
fs.accessSync(existingParent, fs.constants.W_OK | fs.constants.X_OK);
} catch {
findings.push({
checkId: "core/doctor/write-config",
severity: "warning",
message: configPathExists ? "Doctor cannot write config because the config directory is not writable." : "Doctor cannot create the config directory because the nearest existing parent is not writable.",
path: existingParent,
target: configPathExists ? configPath : configDirectory,
requirement: "writable-config-directory",
fixHint: "Make the existing config directory or parent directory writable before running doctor --fix."
});
}
return findings;
}
function findNearestExistingParent(path$1) {
let candidate = path$1;
while (!pathEntryExists(candidate)) {
const parent = path.dirname(candidate);
if (parent === candidate) return candidate;
candidate = parent;
}
return candidate;
}
function pathEntryExists(path) {
if (fs.existsSync(path)) return true;
try {
fs.lstatSync(path);
return true;
} catch {
return false;
}
}
function isDirectoryPath(path) {
try {
return fs.statSync(path).isDirectory();
} catch {
return false;
}
}
async function runFinalConfigValidationHealth(ctx) {
const { readConfigFileSnapshot } = await import("./config/config.js");
const finalSnapshot = await readConfigFileSnapshot({
skipPluginValidation: isUpdateDoctorRun(ctx.env ?? process.env),
preservedLegacyRootKeys: ctx.configResult.preservedLegacyRootKeys
});
if (finalSnapshot.exists && !finalSnapshot.valid) {
ctx.runtime.error("Invalid config:");
for (const issue of finalSnapshot.issues) ctx.runtime.error(`- ${issue.path || "<root>"}: ${issue.message}`);
}
}
//#endregion
export { runWriteConfigHealth as a, runRetiredAuthProfileCleanup as i, runFinalConfigValidationHealth as n, runInitialConfigWriteHealth as r, collectWriteConfigHealthFindings as t };