openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
361 lines (360 loc) • 19.3 kB
JavaScript
import { t as safeParseJson } from "../json-coercion-pfKbZnCJ.mjs";
import { a as writeRuntimeJson, r as defaultRuntime } from "../runtime-BC29JSZp.mjs";
import { t as exitCliAfterOutput } from "../one-shot-exit-CAsLf7Cw.mjs";
import { r as resolveStateDir } from "../state-dir-CfAW6S2K.mjs";
import "../paths-FklbMVOH.mjs";
import { t as UpdateSchemaRefusalError } from "../openclaw-update-schema-refusal-De8B0LH-.mjs";
import { r as formatCliJsonFailure } from "../failure-output-Bu6CUgrO.mjs";
import { i as getNodeSqliteKysely, n as executeSqliteQuerySync, r as executeSqliteQueryTakeFirstSync } from "../kysely-sync-BMSZn9yz.mjs";
import { t as clearNodeSqliteKyselyCacheForDatabase } from "../kysely-sync-cache-state-d4ELXpBH.mjs";
import { t as openNodeSqliteDatabase } from "../node-sqlite-DWTcT2ek.mjs";
import { i as tableExists } from "../openclaw-state-db-schema-helpers-DsjDX6BL.mjs";
import { t as VERSION } from "../version-Dx-YMDJx.mjs";
import { o as getOpenClawDatabaseMaintenanceScope } from "../openclaw-state-db-async-lifecycle-8N3pIeYH.mjs";
import { s as resolveOpenClawStateSqlitePath } from "../openclaw-state-db.paths-BvBEgHyD.mjs";
import { t as prepareSqliteReadOnlyLocation } from "../sqlite-snapshot-source-Qcp-YQWO.mjs";
import "../update-post-core-context-B2x24New.mjs";
import { n as readUpdateRunDriver, r as sameUpdateRunDriver, t as inspectUpdateRunDriver } from "../update-run-driver-C0DjICY9.mjs";
import { r as UpdateRunRecordSchema } from "../update-run-schema-Ap8UiBpH.mjs";
import { n as openDoctorStateSchemaReadAdmission } from "../openclaw-state-db-doctor-schema-CkfI7JQa.mjs";
import { lt as readStateSchemaPublicationBlocker } from "../openclaw-state-db-existing-schema-CUJtAJx4.mjs";
import { t as prepareDoctorDatabasePreflight } from "../doctor-database-preflight-CXfAplVV.mjs";
import { n as recordUpdateDoctorRefusal, r as resolveUpdateDoctorGitRecovery } from "../doctor-update-refusal-Bmut3giK.mjs";
import { u as isUpdatePackageSwapInProgress } from "../update-phase-BjNYi_ax.mjs";
import { lstatSync, realpathSync, statSync } from "node:fs";
import path from "node:path";
import { randomUUID } from "node:crypto";
//#region src/commands/doctor-update-schema-guard.ts
function sameSnapshotFile(left, right) {
return left.dev !== 0 && left.ino !== 0 && left.dev === right.dev && left.ino === right.ino;
}
async function readDrivingUpdater(sharedContentUpgrade = false) {
const snapshot = await prepareSqliteReadOnlyLocation(resolveOpenClawStateSqlitePath(), { preserveSourceArtifacts: true });
try {
const database = openNodeSqliteDatabase(snapshot.location, { readOnly: true });
let closeSchemaReadAdmission;
try {
closeSchemaReadAdmission = openDoctorStateSchemaReadAdmission(database);
if (sharedContentUpgrade && process.platform === "win32" && tableExists(database, "update_runs")) {
const rows = executeSqliteQuerySync(database, getNodeSqliteKysely(database).selectFrom("update_runs").select([
"run_id",
"before_json",
"origin_json",
"steps_json"
]).where("status", "=", "running").orderBy("run_id")).rows;
for (const row of rows) {
const before = UpdateRunRecordSchema.shape.before.safeParse(safeParseJson(row.before_json));
if (!before.success || before.data.version !== "2026.9.4") continue;
const origin = UpdateRunRecordSchema.shape.origin.safeParse(safeParseJson(row.origin_json));
const drivers = origin.success ? [...origin.data.driver ? [origin.data.driver] : [], ...origin.data.previousDrivers ?? []] : [];
const steps = UpdateRunRecordSchema.shape.steps.safeParse(safeParseJson(row.steps_json));
if (steps.success && !steps.data.some((step) => step.step === "driver:identity-unavailable") && drivers.length > 0 && drivers.every((driver) => inspectUpdateRunDriver(driver) === "dead")) continue;
return {
runId: row.run_id,
version: before.data.version,
canDeferStateSchema: false,
earlyDoctorRunning: false,
postCoreStarted: false,
requiresReadableSharedContent: true
};
}
}
const blocker = readStateSchemaPublicationBlocker(database);
if (!blocker) return;
const row = executeSqliteQueryTakeFirstSync(database, getNodeSqliteKysely(database).selectFrom("update_runs").select(["status", "steps_json"]).where("run_id", "=", blocker.runId));
const steps = UpdateRunRecordSchema.shape.steps.safeParse(row ? safeParseJson(row.steps_json) : []);
const stepStatus = (name) => steps.success ? steps.data.findLast((step) => step.step === name)?.status : void 0;
return {
runId: blocker.runId,
version: blocker.updaterVersion,
canDeferStateSchema: tableExists(database, "config_machine_state"),
earlyDoctorRunning: row?.status === "running" && stepStatus("openclaw doctor") === "in_progress",
postCoreStarted: row?.status === "running" && stepStatus("openclaw doctor") === "completed" && stepStatus("post-update verification") === "in_progress"
};
} finally {
try {
closeSchemaReadAdmission?.();
} finally {
clearNodeSqliteKyselyCacheForDatabase(database);
database.close();
}
}
} finally {
await snapshot.cleanupAsync();
}
}
/** Prepare reusable fleet facts and refuse before CLI bootstrap or Doctor can write state. */
async function guardUpdateDoctorSchemaUpgrade(options) {
if (process.env.OPENCLAW_UPDATE_IN_PROGRESS !== "1") return;
const schemas = options.schemas ?? await prepareDoctorDatabasePreflight();
if (!schemas.pendingMigrations?.length) return schemas;
let updater;
const sharedContentUpgrade = options.entry !== void 0 && schemas.pendingMigrations.some((database) => database.kind === "state" && database.supportedVersion > 17);
try {
updater = await readDrivingUpdater(sharedContentUpgrade);
} catch {}
if (options.entry === "package-lifecycle" && !updater?.requiresReadableSharedContent) return schemas;
if (!updater) return schemas;
const blockedMigrations = schemas.pendingMigrations.filter((database) => updater.requiresReadableSharedContent ? database.kind === "state" && database.supportedVersion > 17 : database.kind === "agent" || !updater.canDeferStateSchema);
if (blockedMigrations.length === 0) return schemas;
const postCoreRecovery = {
message: "The update has already committed its package. Complete Doctor repair with the installed compatible build before restarting the Gateway; package rollback cannot undo migrated state.",
commands: ["openclaw doctor --fix", "openclaw gateway start"]
};
const recovery = updater.postCoreStarted ? postCoreRecovery : await resolveUpdateDoctorGitRecovery();
if (updater.canDeferStateSchema && blockedMigrations.every((entry) => entry.kind === "agent")) {
const capturePending = () => blockedMigrations.map((database) => ({
database,
identity: statSync(database.path)
}));
const coverageRefusal = (uncovered, detail) => new UpdateSchemaRefusalError(uncovered, updater.version, {
targetVersion: VERSION,
cause: /* @__PURE__ */ new Error(`Missing recoverable canonical backup coverage: ${detail}`),
recovery
});
const authority = options.postCoreSchemaRepair;
const maintenance = getOpenClawDatabaseMaintenanceScope();
if (authority?.runId === updater.runId && updater.postCoreStarted && maintenance?.ownsSchemaMaintenance) {
const pending = capturePending();
const assertCurrent = () => {
authority.assertCurrent();
maintenance.assertAdmission();
const changed = pending.filter(({ database, identity }) => {
const current = statSync(database.path, { throwIfNoEntry: false });
return !current?.isFile() || !sameSnapshotFile(identity, current);
});
if (changed.length) throw coverageRefusal(changed.map(({ database }) => database), "a pending agent database changed physical identity.");
};
assertCurrent();
const [{ createBackupArchive }, { verifyBackupArchive }, { resolveUpdateCaptureRoot }] = await Promise.all([
import("../backup-create-DEfJ-BwV.mjs"),
import("../backup-verify-DoicyK69.mjs"),
import("../update-capture-paths-C10lojjC.mjs")
]);
assertCurrent();
let snapshotFacts = [];
const backup = await createBackupArchive({
output: path.join(resolveUpdateCaptureRoot(resolveStateDir()), `agent-schema-${updater.runId}-${randomUUID()}.tar.gz`),
includeWorkspace: false,
onSqliteSnapshots: (facts) => {
snapshotFacts = facts;
}
});
postCoreRecovery.message += ` Archive retained for inspection at ${backup.archivePath}.`;
assertCurrent();
const requiredSnapshots = [];
const uncovered = pending.filter(({ database, identity }) => {
const fact = snapshotFacts.find((snapshot) => snapshot.role === "agent" && snapshot.agentId === database.agentId && sameSnapshotFile(identity, snapshot));
if (!fact) return true;
requiredSnapshots.push(fact);
return false;
});
if (uncovered.length) throw coverageRefusal(uncovered.map(({ database }) => database), `the retained archive at ${backup.archivePath} has no captured canonical image for these agent databases.`);
await verifyBackupArchive(backup.archivePath, requiredSnapshots);
assertCurrent();
const current = await readDrivingUpdater();
assertCurrent();
if (current?.runId !== updater.runId || !current.postCoreStarted) throw new UpdateSchemaRefusalError(blockedMigrations, updater.version, {
targetVersion: VERSION,
recovery: postCoreRecovery,
cause: /* @__PURE__ */ new Error(`Post-core admission changed; recovery backup remains at ${backup.archivePath}.`)
});
maintenance.addAgentSchemaMigrationCheck((migration) => {
authority.assertCurrent();
const identity = statSync(migration.path, { throwIfNoEntry: false });
if (!identity?.isFile() || !requiredSnapshots.some((fact) => fact.role === "agent" && fact.agentId === migration.agentId && sameSnapshotFile(identity, fact))) throw coverageRefusal([{
kind: "agent",
...migration
}], "agent database physical identity changed or has no verified snapshot before schema migration.");
});
(options.runtime ?? defaultRuntime).log(`Verified agent-schema recovery backup retained at ${backup.archivePath}.`);
options.onVerifiedBackup?.(requiredSnapshots);
return schemas;
}
if (!recovery && updater.earlyDoctorRunning && !updater.postCoreStarted && isUpdatePackageSwapInProgress(process.env) && process.env.OPENCLAW_UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE === "1" && process.env["OPENCLAW_UPDATE_POST_CORE"] !== "1" && process.env.OPENCLAW_UPDATE_PARENT_SUPPORTS_GATEWAY_RESTART === "1" && process.env.OPENCLAW_UPDATE_PARENT_ALLOWS_GATEWAY_SERVICE_REPAIR === "0" && process.env.OPENCLAW_UPDATE_PARENT_ALLOWS_GATEWAY_ACTIVATION === "0" && process.env.OPENCLAW_SERVICE_REPAIR_POLICY === "external") {
const pending = capturePending();
const registered = schemas.agentDatabaseMigrationDiscovery?.registeredAgentDatabases ?? [];
const uncovered = pending.filter(({ database, identity }) => !registered.some((entry) => {
if (entry.agentId !== database.agentId) return false;
const current = statSync(entry.path, { throwIfNoEntry: false });
return current?.isFile() && sameSnapshotFile(identity, current);
}));
if (uncovered.length) throw coverageRefusal(uncovered.map(({ database }) => database), "these pending agent databases have no registered canonical snapshot owner.");
return {
...schemas,
updateSchemaRehearsal: {
runId: updater.runId,
updaterVersion: updater.version
}
};
}
}
const error = new UpdateSchemaRefusalError(blockedMigrations, updater.version, {
targetVersion: VERSION,
recovery
});
if (recovery) recordUpdateDoctorRefusal(error.message);
if (options.json) {
const runtime = options.runtime ?? defaultRuntime;
writeRuntimeJson(runtime, formatCliJsonFailure(error));
exitCliAfterOutput(runtime, 1);
}
throw error;
}
/** Complete a private CLI validation before bootstrap can reach any live writer. */
async function preflightUpdateDoctorCli(options) {
const parent = readUpdateRunDriver(process.ppid);
const schemas = await guardUpdateDoctorSchemaUpgrade({
...options,
entry: "cli-bootstrap"
});
if (schemas?.updateSchemaRehearsal) {
await rehearseDeferredUpdateDoctorSchemaForParent(schemas, defaultRuntime, parent);
exitCliAfterOutput(defaultRuntime, 0);
}
return schemas;
}
/** The shipped Windows driver can still discard a failed npm stage before entering repair. */
async function preflightUpdatePackageLifecycle() {
if (process.platform !== "win32" || process.env.OPENCLAW_UPDATE_IN_PROGRESS !== "1") return;
let updater;
try {
updater = await readDrivingUpdater(true);
} catch {}
if (!updater?.requiresReadableSharedContent) return;
await guardUpdateDoctorSchemaUpgrade({
schemas: await prepareDoctorDatabasePreflight({ scope: "state" }),
entry: "package-lifecycle"
});
}
/** The shipped package validator may still roll back; it must never reach live Doctor writers. */
function rehearseDeferredUpdateDoctorSchema(schemas, runtime = defaultRuntime) {
return rehearseDeferredUpdateDoctorSchemaForParent(schemas, runtime, readUpdateRunDriver(process.ppid));
}
async function rehearseDeferredUpdateDoctorSchemaForParent(schemas, runtime, parent) {
const selected = schemas.updateSchemaRehearsal;
if (!selected) throw new Error("Missing legacy update schema rehearsal admission.");
if (!parent) throw new Error("The schema rehearsal parent identity is unavailable.");
const assertParent = () => {
const current = readUpdateRunDriver(process.ppid);
if (!current || !sameUpdateRunDriver(current, parent)) throw new Error("The schema rehearsal parent identity changed.");
};
assertParent();
const [{ createConfigIO }, { resolveOpenClawPackageRoot }, { resolveUpdateInstallKind }, { prepareUpdateCandidateRehearsal }, { resolveGatewayInstallEntrypoint }, { runUtf8CommandWithTimeout }, { resolveSqliteInspectionBudget }] = await Promise.all([
import("../io-BFXJQ9PP.mjs"),
import("../openclaw-root-D92EvOWI.mjs"),
import("../update-check-CZjfp02f.mjs"),
import("../update-candidate-rehearsal-DBIbb5c4.mjs"),
import("../gateway-entrypoint-CnuP5efC.mjs"),
import("../exec-B5ecvzP7.mjs"),
import("../sqlite-readonly-worker-CIug2ylD.mjs")
]);
const root = await resolveOpenClawPackageRoot({
moduleUrl: import.meta.url,
argv1: process.argv[1]
});
const updater = await readDrivingUpdater();
if (!root || await resolveUpdateInstallKind(root) !== "package" || updater?.runId !== selected.runId || !updater.earlyDoctorRunning || updater.postCoreStarted) throw new UpdateSchemaRefusalError(schemas.pendingMigrations ?? [], selected.updaterVersion, {
targetVersion: VERSION,
cause: /* @__PURE__ */ new Error("The shipped package-validation handoff could not be verified.")
});
assertParent();
const entry = await resolveGatewayInstallEntrypoint(root);
if (!entry) throw new Error("Candidate Doctor entrypoint is unavailable for private schema validation.");
const snapshot = await createConfigIO({
observe: false,
pluginValidation: "core-only"
}).readConfigFileSnapshot();
assertParent();
const rehearsal = await prepareUpdateCandidateRehearsal({
candidateRoot: root,
config: snapshot.sourceConfig ?? snapshot.config,
stateDir: resolveStateDir()
});
const cleanupRoots = new Map(rehearsal.cleanupDirectories.map((directory) => {
const identity = lstatSync(directory);
if (!identity.isDirectory() || identity.isSymbolicLink() || realpathSync(directory) !== directory) throw new Error(`Private Doctor cleanup root is not physical; retained ${directory}.`);
return [directory, identity];
}));
const assertCleanupDirectory = (directory) => {
const original = cleanupRoots.get(directory);
const current = lstatSync(directory);
if (!original || !sameSnapshotFile(original, current) || !current.isDirectory() || current.isSymbolicLink() || current.uid !== original.uid || realpathSync(directory) !== directory) throw new Error(`Private Doctor cleanup identity changed; inspect ${directory}.`);
};
let failure;
let settled = false;
let writerStarted = false;
let resourceWarnings = [];
try {
const env = {
...rehearsal.env,
OPENCLAW_UPDATE_IN_PROGRESS: "0"
};
const { inspectPreparedDoctorRehearsal } = await import("../doctor-update-rehearsal-inventory-Bz0RvJa1.mjs");
const admitted = await inspectPreparedDoctorRehearsal({
stateDir: rehearsal.stateDir,
pluginCodeLinks: rehearsal.pluginCodeLinks,
env,
assertCurrent: assertParent
});
const current = await readDrivingUpdater();
if (current?.runId !== selected.runId || !current.earlyDoctorRunning || current.postCoreStarted) throw new Error("The schema rehearsal updater changed before Doctor launch.");
resourceWarnings = admitted.fact.warnings;
runtime.log(JSON.stringify(admitted.fact));
admitted.assertPrepared();
writerStarted = true;
const result = await runUtf8CommandWithTimeout([
process.execPath,
entry,
"doctor",
"--fix",
"--non-interactive",
"--no-workspace-suggestions"
], {
cwd: root,
baseEnv: env,
timeoutMs: resolveSqliteInspectionBudget("legacy update schema rehearsal", rehearsal.stateDir, rehearsal.snapshotCapacity.sqliteBytes).timeoutMs,
killProcessTree: true,
requireProcessTreeExtinction: true,
outputCapture: {
stdout: "discard",
stderr: "tail"
},
maxOutputBytes: { stderr: 2e4 }
});
settled = result.cleanup === "normal";
if (!settled) throw new Error(`Private Doctor did not settle; rehearsal retained at ${rehearsal.stateDir}.`);
if (result.code !== 0 || result.termination !== "exit" || result.outputLimitExceeded) {
const { redactSupportString } = await import("../diagnostic-support-redaction-pE-trMfR.mjs");
throw new Error(`Private Doctor schema validation failed (${result.termination}): ${redactSupportString(result.stderr, {
env,
stateDir: rehearsal.stateDir
}, { maxLength: 2e3 })}`);
}
} catch (error) {
failure = { error };
}
if (settled || !writerStarted) try {
for (const directory of cleanupRoots.keys()) assertCleanupDirectory(directory);
await rehearsal.cleanup(assertCleanupDirectory);
} catch (cleanupError) {
if (failure) runtime.error(`Warning: Private Doctor cleanup was not completed: ${String(cleanupError)}`);
else throw cleanupError;
}
if (failure) throw failure.error;
const warning = `Validated schema repair on private copies for OpenClaw ${selected.updaterVersion}; live agent databases are unchanged. Repair is deferred to the fresh post-core updater.`;
const { UPDATE_POST_INSTALL_DOCTOR_RESULT_PATH_ENV, writeUpdatePostInstallDoctorResult } = await import("../update-doctor-result-Da8Leeli.mjs");
const resultPath = process.env[UPDATE_POST_INSTALL_DOCTOR_RESULT_PATH_ENV];
if (resultPath) await writeUpdatePostInstallDoctorResult({
resultPath,
result: {
status: "ok",
configHash: "unchanged",
warnings: [warning, ...resourceWarnings.map((resourceWarning) => resourceWarning.message)]
}
});
for (const resourceWarning of resourceWarnings) runtime.error(`Warning: ${resourceWarning.message}`);
runtime.log(warning);
}
//#endregion
export { guardUpdateDoctorSchemaUpgrade, preflightUpdateDoctorCli, preflightUpdatePackageLifecycle, rehearseDeferredUpdateDoctorSchema };