openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,614 lines • 73.7 kB
JavaScript
import { E as resolveDateTimestampMs } from "./number-coercion-CLj0HTDM.js";
import { n as collectErrorGraphCandidates } from "./error-coercion-D_-xJ90S.js";
import { a as writeRuntimeJson } from "./runtime-CF2WjnNZ.js";
import { c as resolveUserPath } from "./home-dir-BPhrG-aM.js";
import { t as createLazyImportLoader } from "./lazy-promise-DGqyc4Y4.js";
import { r as createLazyRuntimeModule } from "./lazy-runtime-CgCh8H_K.js";
import { n as isErrno, t as hasErrnoCode } from "./errno-CkbDOfLk.js";
import { I as sameFileIdentity, w as root } from "./fs-safe-B6pvPGnf.js";
import { f as resolveHomeDir } from "./utils-P__uGsPB.js";
import { t as sleep } from "./sleep-D7nua6TP.js";
import { n as normalizeAgentId } from "./agent-id-CeT3w4ap.js";
import { g as resolveGatewayLockDir } from "./paths-D2sRr1a_.js";
import "./session-key-BnWWjqNc.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { h as writeJson } from "./json-files-Bq1lIlQB.js";
import { a as resolveSqliteFilesystemPath, n as requireNodeSqlite, t as openNodeSqliteDatabase } from "./node-sqlite-BpQX3W0e.js";
import { a as getNodeSqliteKysely, r as executeSqliteQuerySync } from "./kysely-sync-COmh4HWh.js";
import { c as createPrivateSqliteTempDirectory, o as withSqliteSnapshotSource } from "./sqlite-readonly-location-BC9PgENz.js";
import { u as assertOpenClawStateDatabaseOwner } from "./openclaw-state-db-cache-C7ljO0xP.js";
import { l as resolveRuntimeServiceVersion } from "./version-v1kuAkGj.js";
import { a as readSqliteUserVersion } from "./sqlite-user-version-DFJCxX41.js";
import { s as resolveOpenClawStateSqlitePath } from "./openclaw-state-db-schema-version-c1ZL6JGz.js";
import { r as withExistingOpenClawStateDatabaseReadOnly } from "./openclaw-state-db-readonly-BRgmrGHt.js";
import { t as assertSqliteIntegrity } from "./sqlite-integrity-NpEtFIdK.js";
import { dt as resolveSqliteDatabaseFilePaths, lt as SQLITE_SIDECAR_SUFFIXES } from "./openclaw-state-db-BRTnL-D8.js";
import { n as CONFIG_AUDIT_SCOPE } from "./io.audit-Dor_5i6r.js";
import { a as publishFileExclusive, d as syncDirectoryIfSupported, i as pinDirectory, l as syncDirectory, n as getPublishFileExclusiveFailureDetails, o as publishFileNoClobber, r as isHardlinkFallbackError, s as requireDirectorySync } from "./directory-durability-CINgXRM4.js";
import { n as assertOpenClawAgentDatabaseOwner } from "./openclaw-agent-db-maintenance-wTIy-jt-.js";
import { t as loadSqliteVecExtension } from "./sqlite-vec-C8wbuI1F.js";
import { f as tryParsePersistedExecApprovals } from "./exec-approvals-config-D1lCGl0_.js";
import { o as projectionValues } from "./exec-approvals-sqlite-BK42k6JF.js";
import "./engine-storage-Dk7qCapl.js";
import { n as SYSTEM_AGENT_AUDIT_SCOPE } from "./audit-D3_LINzS.js";
import { n as isPathWithin } from "./cleanup-utils-BDhDB9DZ.js";
import { n as createBackupVolatileStatCache, t as createBackupLinkCache } from "./backup-volatile-stat-cache-CCe7X9Xy.js";
import { c as buildBackupArchiveRoot, h as isTransientSqliteBackupPath, l as canonicalizePathForContainment, o as buildBackupArchiveBasename, p as resolveBackupPlanFromDisk, s as buildBackupArchivePath, t as assertArchiveSymbolicLinkTarget, u as recordBackupOutcomeBestEffort } from "./backup-archive-path-policy-BwxoeKmU.js";
import { a as readLegacyAuditSourcePrefixSnapshotForBackup, d as prepareLegacyAuditRecords, f as serializePreparedAuditRecords, g as legacyAuditSourceGenerationKey, h as legacyAuditRawCheckpointKey, i as readLegacyAuditRecoverySourceForBackup, p as detectLegacyAuditLogs, r as findPreviousLegacyAuditRawCheckpoint, t as withLegacyAuditMigrationLease } from "./state-migrations.audit-coordination-CJaFTJ5E.js";
import fs, { createWriteStream, realpathSync } from "node:fs";
import path from "node:path";
import fs$1 from "node:fs/promises";
import os from "node:os";
import { createHash, randomUUID } from "node:crypto";
import { Transform } from "node:stream";
import { pipeline as pipeline$1 } from "node:stream/promises";
//#region src/infra/sqlite-snapshot.ts
async function assertRegularSourceFile(sourcePath, requireNonEmptySource) {
const stat = await fs$1.lstat(sourcePath);
if (!stat.isFile()) throw new Error(`SQLite snapshot source must be a regular file: ${sourcePath}`);
if (requireNonEmptySource && stat.size === 0) throw new Error(`SQLite snapshot source must not be empty: ${sourcePath}`);
}
async function assertTargetAbsent$1(targetPath) {
try {
await fs$1.lstat(targetPath);
} catch (error) {
if (error.code === "ENOENT") return;
throw error;
}
throw new Error(`SQLite snapshot target already exists: ${targetPath}`);
}
async function copyFileExclusive(source, targetPath) {
const sourceFingerprint = await readMutationFingerprint(source);
let target;
let targetIdentity;
try {
target = await fs$1.open(targetPath, "wx+", 384);
targetIdentity = await target.stat();
const buffer = Buffer.allocUnsafe(1048576);
const hash = createHash("sha256");
let offset = 0;
while (true) {
const { bytesRead } = await source.read(buffer, 0, buffer.length, offset);
if (bytesRead === 0) break;
hash.update(buffer.subarray(0, bytesRead));
let bytesWritten = 0;
while (bytesWritten < bytesRead) {
const result = await target.write(buffer, bytesWritten, bytesRead - bytesWritten, offset + bytesWritten);
if (result.bytesWritten === 0) throw new Error(`SQLite snapshot copy made no progress: ${targetPath}`);
bytesWritten += result.bytesWritten;
}
offset += bytesRead;
}
await assertMutationFingerprintUnchanged(source, sourceFingerprint, targetPath);
await target.sync();
const currentIdentity = await fs$1.lstat(targetPath);
if (!sameFileIdentity(targetIdentity, currentIdentity)) throw new Error(`SQLite snapshot target changed during publication: ${targetPath}`);
return {
content: {
sha256: hash.digest("hex"),
sizeBytes: offset
},
identity: currentIdentity
};
} catch (error) {
if (targetIdentity) {
await target?.close().catch(() => void 0);
target = void 0;
removePublishedTargetIfOwned(targetPath, targetIdentity);
}
throw error;
} finally {
await target?.close().catch(() => void 0);
}
}
async function readMutationFingerprint(handle) {
const stat = await handle.stat({ bigint: true });
return {
birthtimeNs: stat.birthtimeNs,
ctimeNs: stat.ctimeNs,
dev: stat.dev,
ino: stat.ino,
mtimeNs: stat.mtimeNs,
size: stat.size
};
}
async function assertMutationFingerprintUnchanged(handle, expected, filePath) {
const current = await readMutationFingerprint(handle);
if (current.birthtimeNs !== expected.birthtimeNs || current.ctimeNs !== expected.ctimeNs || current.dev !== expected.dev || current.ino !== expected.ino || current.mtimeNs !== expected.mtimeNs || current.size !== expected.size) throw new Error(`SQLite snapshot file changed while reading: ${filePath}`);
}
function sameMutationFingerprint(left, right) {
return left.birthtimeNs === right.birthtimeNs && left.ctimeNs === right.ctimeNs && left.dev === right.dev && left.ino === right.ino && left.mtimeNs === right.mtimeNs && left.size === right.size;
}
async function syncFile(filePath) {
const handle = await fs$1.open(filePath, "r+");
try {
await handle.sync();
} finally {
await handle.close();
}
}
async function assertOpenFileIdentity(handle, filePath, expectedIdentity) {
const openedIdentity = await handle.stat();
const currentIdentity = await fs$1.lstat(filePath);
if (!openedIdentity.isFile() || !currentIdentity.isFile() || !sameFileIdentity(expectedIdentity, openedIdentity) || !sameFileIdentity(expectedIdentity, currentIdentity)) throw new Error(`SQLite snapshot file changed: ${filePath}`);
}
async function hashPublishedFile(filePath, expectedIdentity) {
const handle = await fs$1.open(filePath, "r");
try {
return await hashOpenPublishedFile(handle, filePath, expectedIdentity);
} finally {
await handle.close();
}
}
async function hashOpenPublishedFile(handle, filePath, expectedIdentity) {
await assertOpenFileIdentity(handle, filePath, expectedIdentity);
const fingerprint = await readMutationFingerprint(handle);
const buffer = Buffer.allocUnsafe(1048576);
const hash = createHash("sha256");
let offset = 0;
while (true) {
const { bytesRead } = await handle.read(buffer, 0, buffer.length, offset);
if (bytesRead === 0) break;
hash.update(buffer.subarray(0, bytesRead));
offset += bytesRead;
}
await assertMutationFingerprintUnchanged(handle, fingerprint, filePath);
await assertOpenFileIdentity(handle, filePath, expectedIdentity);
return {
sha256: hash.digest("hex"),
sizeBytes: offset
};
}
function assertPublishedFileIdentitySync(filePath, expectedIdentity) {
const currentIdentity = fs.lstatSync(filePath);
if (!currentIdentity.isFile() || !sameFileIdentity(expectedIdentity, currentIdentity) || expectedIdentity.size !== currentIdentity.size || expectedIdentity.mtimeMs !== currentIdentity.mtimeMs || expectedIdentity.ctimeMs !== currentIdentity.ctimeMs || expectedIdentity.birthtimeMs !== currentIdentity.birthtimeMs) throw new Error(`SQLite snapshot file changed: ${filePath}`);
}
function assertOpenFileIdentitySync(fileDescriptor, filePath, expectedIdentity) {
const openedIdentity = fs.fstatSync(fileDescriptor);
const currentIdentity = fs.lstatSync(filePath);
if (!openedIdentity.isFile() || !currentIdentity.isFile() || !sameFileIdentity(expectedIdentity, openedIdentity) || !sameFileIdentity(expectedIdentity, currentIdentity)) throw new Error(`SQLite snapshot file changed: ${filePath}`);
}
function hashPublishedFileSync(filePath, expectedIdentity) {
const fileDescriptor = fs.openSync(filePath, "r");
try {
assertOpenFileIdentitySync(fileDescriptor, filePath, expectedIdentity);
const initialStat = fs.fstatSync(fileDescriptor, { bigint: true });
const initialFingerprint = {
birthtimeNs: initialStat.birthtimeNs,
ctimeNs: initialStat.ctimeNs,
dev: initialStat.dev,
ino: initialStat.ino,
mtimeNs: initialStat.mtimeNs,
size: initialStat.size
};
const hash = createHash("sha256");
const buffer = Buffer.allocUnsafe(1048576);
let offset = 0;
while (true) {
const bytesRead = fs.readSync(fileDescriptor, buffer, 0, buffer.length, offset);
if (bytesRead === 0) break;
hash.update(buffer.subarray(0, bytesRead));
offset += bytesRead;
}
const finalStat = fs.fstatSync(fileDescriptor, { bigint: true });
if (!sameMutationFingerprint(initialFingerprint, {
birthtimeNs: finalStat.birthtimeNs,
ctimeNs: finalStat.ctimeNs,
dev: finalStat.dev,
ino: finalStat.ino,
mtimeNs: finalStat.mtimeNs,
size: finalStat.size
})) throw new Error(`SQLite snapshot file changed while reading: ${filePath}`);
assertOpenFileIdentitySync(fileDescriptor, filePath, expectedIdentity);
return {
sha256: hash.digest("hex"),
sizeBytes: offset
};
} finally {
fs.closeSync(fileDescriptor);
}
}
function assertExpectedContent(actual, expected, filePath) {
if (actual.sizeBytes !== expected.sizeBytes) throw new Error(`SQLite snapshot size mismatch for ${filePath}: expected ${expected.sizeBytes}, got ${actual.sizeBytes}`);
if (actual.sha256 !== expected.sha256) throw new Error(`SQLite snapshot hash mismatch for ${filePath}: expected ${expected.sha256}, got ${actual.sha256}`);
}
function removePublishedTargetIfOwned(filePath, expectedIdentity, requireFingerprint = false) {
let currentIdentity;
try {
currentIdentity = fs.lstatSync(filePath);
} catch {
return false;
}
const fingerprintMatches = !requireFingerprint || typeof expectedIdentity.size === "number" && typeof expectedIdentity.mtimeMs === "number" && typeof expectedIdentity.ctimeMs === "number" && typeof expectedIdentity.birthtimeMs === "number" && expectedIdentity.size === currentIdentity.size && expectedIdentity.mtimeMs === currentIdentity.mtimeMs && expectedIdentity.ctimeMs === currentIdentity.ctimeMs && expectedIdentity.birthtimeMs === currentIdentity.birthtimeMs;
if (!sameFileIdentity(expectedIdentity, currentIdentity) || !fingerprintMatches) return false;
try {
fs.unlinkSync(filePath);
return true;
} catch {
return false;
}
}
function sameFileStatFingerprint(left, right) {
return sameFileIdentity(left, right) && left.size === right.size && left.mtimeMs === right.mtimeMs && left.birthtimeMs === right.birthtimeMs;
}
function assertSynchronousCallbackResult(result, label) {
if (result && (typeof result === "object" || typeof result === "function") && typeof result.then === "function") {
Promise.resolve(result).catch(() => void 0);
throw new Error(`${label} must be synchronous.`);
}
}
/**
* Publish the exact bytes of one already-verified SQLite file without reopening
* its pathname during the copy. The target is always created exclusively.
*/
async function publishVerifiedSqliteFile(options) {
await assertTargetAbsent$1(options.targetPath);
const targetDirectory = path.resolve(path.dirname(options.targetPath));
const targetDirectoryPin = await pinDirectory(targetDirectory, { label: "SQLite publication directory" });
const targetDirectoryReceipt = targetDirectoryPin.receipt;
let stagingDir;
try {
stagingDir = await createPrivateSqliteTempDirectory(targetDirectory, `.sqlite-publish-${randomUUID()}-`);
} catch (error) {
await targetDirectoryPin.close().catch(() => void 0);
throw error;
}
const stagedPath = path.join(stagingDir, "database.sqlite");
let stagingIdentity;
let source;
let target;
let targetPinFileDescriptor;
let failedPublicationIdentity;
let publishedIdentity;
try {
stagingIdentity = await fs$1.lstat(stagingDir);
await fs$1.chmod(stagingDir, 448);
source = await fs$1.open(options.sourcePath, "r");
await assertOpenFileIdentity(source, options.sourcePath, options.sourceIdentity);
const staged = await copyFileExclusive(source, stagedPath);
const expectedContent = options.expectedContent;
assertExpectedContent(staged.content, expectedContent, options.targetPath);
await source.close();
source = void 0;
await options.validatePublished?.(stagedPath);
assertExpectedContent(await hashPublishedFile(stagedPath, staged.identity), expectedContent, options.targetPath);
await options.beforePublish?.();
await assertTargetAbsent$1(options.targetPath);
const currentStagedIdentity = await fs$1.lstat(stagedPath);
if (!sameFileStatFingerprint(staged.identity, currentStagedIdentity)) throw new Error(`SQLite snapshot staging file changed during publication: ${stagedPath}`);
try {
publishedIdentity = (await publishFileNoClobber(stagedPath, options.targetPath, {
strategy: options.requireAtomicPublication ? "link-required" : "link-or-copy",
durability: "fail-closed"
})).identity;
} catch (error) {
const details = getPublishFileExclusiveFailureDetails(error);
const stagedAfterFailure = details?.targetCreated ? await fs$1.lstat(stagedPath).catch(() => void 0) : void 0;
const targetAfterFailure = details?.targetCreated ? await fs$1.lstat(options.targetPath).catch(() => void 0) : void 0;
const stagedPathChanged = !stagedAfterFailure || !sameFileStatFingerprint(staged.identity, stagedAfterFailure);
if (details?.targetCreated && details.cleanup !== "removed" && stagedAfterFailure && targetAfterFailure && sameFileIdentity(stagedAfterFailure, targetAfterFailure)) failedPublicationIdentity = targetAfterFailure;
else if (details?.targetCreated && details.cleanup !== "removed" && details.targetIdentity && targetAfterFailure && sameFileIdentity(details.targetIdentity, targetAfterFailure)) failedPublicationIdentity = targetAfterFailure;
if (options.requireAtomicPublication && isHardlinkFallbackError(error)) throw new Error(`Atomic SQLite publication requires hard-link support in ${targetDirectory}.`, { cause: error });
if (details?.targetCreated) {
if (stagedPathChanged) throw new Error(`SQLite snapshot staging file changed during publication: ${options.targetPath}`, { cause: error });
throw new Error(`SQLite snapshot target changed during publication: ${options.targetPath}`, { cause: error });
}
throw error;
}
if (!publishedIdentity) throw new Error(`SQLite snapshot target was not published: ${options.targetPath}`);
const initialPublishedIdentity = publishedIdentity;
target = await fs$1.open(options.targetPath, "r");
await assertOpenFileIdentity(target, options.targetPath, initialPublishedIdentity);
await fs$1.unlink(stagedPath);
const expectedIdentity = await target.stat();
publishedIdentity = expectedIdentity;
assertExpectedContent(await hashOpenPublishedFile(target, options.targetPath, expectedIdentity), expectedContent, options.targetPath);
await fs$1.rmdir(stagingDir);
requireDirectorySync(await syncDirectory(targetDirectoryReceipt), "SQLite publication directory");
await target.close();
target = void 0;
targetPinFileDescriptor = fs.openSync(options.targetPath, "r");
assertOpenFileIdentitySync(targetPinFileDescriptor, options.targetPath, expectedIdentity);
const guard = {
assertTargetMatchesExpectedContent: (finalCheck) => {
assertExpectedContent(hashPublishedFileSync(options.targetPath, expectedIdentity), expectedContent, options.targetPath);
assertSynchronousCallbackResult(finalCheck?.(), "SQLite publication final check");
assertPublishedFileIdentitySync(options.targetPath, expectedIdentity);
},
assertTargetUnchanged: (finalCheck) => {
assertPublishedFileIdentitySync(options.targetPath, expectedIdentity);
assertSynchronousCallbackResult(finalCheck?.(), "SQLite publication final check");
assertPublishedFileIdentitySync(options.targetPath, expectedIdentity);
}
};
if (options.afterPublish) assertSynchronousCallbackResult(options.afterPublish(guard), "SQLite after-publication guard");
else guard.assertTargetUnchanged();
fs.closeSync(targetPinFileDescriptor);
targetPinFileDescriptor = void 0;
} catch (error) {
if (target && publishedIdentity) {
const openedIdentity = await target.stat().catch(() => void 0);
if (openedIdentity && sameFileIdentity(openedIdentity, publishedIdentity)) publishedIdentity = openedIdentity;
}
const cleanupIdentity = publishedIdentity ?? failedPublicationIdentity;
if (cleanupIdentity) {
if (removePublishedTargetIfOwned(options.targetPath, cleanupIdentity, true)) await syncDirectory(targetDirectoryReceipt).catch(() => void 0);
}
if (stagingIdentity) await removePublicationStagingDirectory(stagingDir, stagingIdentity).catch(() => void 0);
else await fs$1.rmdir(stagingDir).catch(() => void 0);
throw error;
} finally {
if (targetPinFileDescriptor !== void 0) fs.closeSync(targetPinFileDescriptor);
if (target) await target.close().catch(() => void 0);
if (source) await source.close().catch(() => void 0);
await targetDirectoryPin.close().catch(() => void 0);
}
}
async function removePublicationStagingDirectory(stagingDir, expectedIdentity) {
const currentIdentity = await fs$1.lstat(stagingDir).catch(() => void 0);
if (!currentIdentity) return;
if (!currentIdentity.isDirectory() || !sameFileIdentity(expectedIdentity, currentIdentity)) throw new Error(`SQLite publication staging directory changed: ${stagingDir}`);
const entries = await fs$1.readdir(stagingDir, { withFileTypes: true });
if (entries.length > 1 || entries.some((entry) => entry.name !== "database.sqlite" || !entry.isFile())) throw new Error(`SQLite publication staging directory has unexpected contents: ${stagingDir}`);
const stagedEntry = entries[0];
if (stagedEntry) await fs$1.unlink(path.join(stagingDir, stagedEntry.name));
await fs$1.rmdir(stagingDir);
}
/**
* Compact one SQLite database into a fresh private file and verify the result.
*
* The source and output both receive full structural, index, and foreign-key
* checks. Only a fully verified, synced snapshot is published to the target.
*/
async function createVerifiedSqliteSnapshot(options) {
await assertRegularSourceFile(options.sourcePath, options.requireNonEmptySource === true);
await assertTargetAbsent$1(options.targetPath);
const stagingDir = await createPrivateSqliteTempDirectory(path.dirname(options.targetPath), ".sqlite-snapshot-");
await fs$1.chmod(stagingDir, 448);
const stagedPath = path.join(stagingDir, "database.sqlite");
const sqlite = requireNodeSqlite();
let stagedIdentity;
try {
await withSqliteSnapshotSource(options.sourcePath, async (snapshotSourcePath) => {
await fs$1.rm(stagedPath, { force: true });
const source = openNodeSqliteDatabase(snapshotSourcePath, {
allowExtension: true,
readOnly: true
});
try {
source.exec("PRAGMA busy_timeout = 30000; PRAGMA trusted_schema = OFF; BEGIN;");
try {
source.prepare("PRAGMA schema_version;").get();
await loadSqliteVecExtension({ db: source });
assertSqliteIntegrity(source, options.sourcePath);
options.validate?.(source, options.sourcePath);
await sqlite.backup(source, resolveSqliteFilesystemPath(stagedPath));
} finally {
source.exec("ROLLBACK;");
}
} finally {
if (source.isOpen) source.close();
}
});
await fs$1.chmod(stagedPath, 384);
const snapshot = openNodeSqliteDatabase(stagedPath, { allowExtension: true });
try {
snapshot.exec("PRAGMA busy_timeout = 30000; PRAGMA trusted_schema = OFF;");
await loadSqliteVecExtension({ db: snapshot });
snapshot.exec("PRAGMA journal_mode = DELETE;");
if (options.transform) await options.transform(snapshot);
snapshot.exec("VACUUM;");
assertSqliteIntegrity(snapshot, options.targetPath);
options.validate?.(snapshot, options.targetPath);
const userVersion = readSqliteUserVersion(snapshot);
snapshot.close();
await syncFile(stagedPath);
stagedIdentity = await fs$1.lstat(stagedPath);
const expectedContent = await hashPublishedFile(stagedPath, stagedIdentity);
await publishVerifiedSqliteFile({
sourceIdentity: stagedIdentity,
sourcePath: stagedPath,
targetPath: options.targetPath,
expectedContent,
beforePublish: options.beforePublish,
afterPublish: options.afterPublish,
validatePublished: async (publishedPath) => {
const published = openNodeSqliteDatabase(publishedPath, {
allowExtension: true,
readOnly: true
});
try {
published.exec("PRAGMA busy_timeout = 30000; PRAGMA trusted_schema = OFF;");
await loadSqliteVecExtension({ db: published });
assertSqliteIntegrity(published, options.targetPath);
options.validate?.(published, options.targetPath);
const publishedUserVersion = readSqliteUserVersion(published);
if (publishedUserVersion !== userVersion) throw new Error(`SQLite snapshot user_version changed during publication: expected ${userVersion}, got ${publishedUserVersion}`);
} finally {
published.close();
}
}
});
return {
path: options.targetPath,
userVersion
};
} finally {
if (snapshot.isOpen) snapshot.close();
}
} catch (error) {
throw new Error(`SQLite database cannot be snapshotted safely: ${options.sourcePath}. ${formatErrorMessage(error)}`, { cause: error });
} finally {
await fs$1.rm(stagingDir, {
force: true,
recursive: true
}).catch(() => void 0);
}
}
//#endregion
//#region src/state/openclaw-state-snapshot-sanitizer.ts
const FAIL_CLOSED_EXEC_APPROVALS = {
version: 1,
defaults: {
security: "deny",
ask: "off",
askFallback: "deny",
autoAllowSkills: false
},
agents: {}
};
function tableExists(database, tableName) {
return database.prepare("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = ?").get(tableName)?.ok === 1;
}
/** Remove coordination rows that must never survive restore. */
function sanitizeOpenClawStateLeaseRows(database) {
if (tableExists(database, "state_leases")) database.prepare("DELETE FROM state_leases").run();
}
/** Remove transient rows whose restoration would replay work or extend private-data retention. */
function sanitizeOpenClawGlobalStateSnapshot(database) {
sanitizeOpenClawStateLeaseRows(database);
if (tableExists(database, "delivery_queue_entries")) database.prepare("DELETE FROM delivery_queue_entries").run();
if (tableExists(database, "plugin_blob_entries")) database.prepare("DELETE FROM plugin_blob_entries WHERE expires_at IS NOT NULL").run();
if (tableExists(database, "exec_approvals_config")) {
const stateDb = getNodeSqliteKysely(database);
const rows = executeSqliteQuerySync(database, stateDb.selectFrom("exec_approvals_config").select(["config_key", "raw_json"])).rows;
for (const row of rows) {
let sanitized = FAIL_CLOSED_EXEC_APPROVALS;
const parsed = tryParsePersistedExecApprovals(row.raw_json);
if (parsed) {
sanitized = structuredClone(parsed);
if (sanitized.socket) delete sanitized.socket.token;
}
executeSqliteQuerySync(database, stateDb.updateTable("exec_approvals_config").set({
raw_json: `${JSON.stringify(sanitized, null, 2)}\n`,
...projectionValues(sanitized)
}).where("config_key", "=", row.config_key));
}
}
}
//#endregion
//#region src/infra/backup-create-stream.ts
const BACKUP_ARCHIVE_IDLE_TIMEOUT_MS = 3e5;
function observeBackupTarEntryProgress(entry, reportProgress) {
const wasFlowing = entry.flowing;
entry.on("data", (chunk) => {
reportProgress(typeof chunk === "string" ? Buffer.byteLength(chunk) : chunk.length);
});
if (!wasFlowing) entry.pause();
}
function removePreparedBackupArchive(prepared) {
let currentIdentity;
try {
currentIdentity = fs.lstatSync(prepared.archivePath);
} catch {
return false;
}
if (!currentIdentity.isFile() || !sameFileIdentity(prepared.identity, currentIdentity)) return false;
try {
fs.unlinkSync(prepared.archivePath);
return true;
} catch {
return false;
}
}
async function writeArchiveStreamToFile(params) {
const idleTimeoutMs = params.idleTimeoutMs ?? BACKUP_ARCHIVE_IDLE_TIMEOUT_MS;
const controller = new AbortController();
let archiveStream;
let openedIdentity;
let idleTimer;
let idleTimeoutError;
let lastEntryPath;
let lastProgress;
let outputBytes = 0;
let producerBytes = 0;
let settled = false;
const reportProgress = (progress) => {
if (settled) return;
if (progress) {
lastProgress = progress;
if (progress.entryPath) lastEntryPath = progress.entryPath;
if (progress.bytes) {
if (progress.phase === "output") outputBytes += progress.bytes;
else if (progress.phase === "raw") producerBytes += progress.bytes;
}
}
idleTimer = idleTimer?.refresh() ?? setTimeout(() => {
const entrySuffix = lastEntryPath ? `, entry=${JSON.stringify(lastEntryPath.slice(-512))}` : "";
idleTimeoutError = /* @__PURE__ */ new Error(`Backup archive write stalled: no progress observed for ${idleTimeoutMs}ms (phase=${lastProgress?.phase ?? "starting"}${entrySuffix}, rawBytes=${producerBytes}, outputBytes=${outputBytes})`);
archiveStream?.destroy(idleTimeoutError);
controller.abort(idleTimeoutError);
}, idleTimeoutMs);
};
const progress = new Transform({ transform(chunk, _encoding, callback) {
reportProgress({
phase: "output",
bytes: chunk.length
});
callback(null, chunk);
} });
const archiveWriteStream = createWriteStream(params.archivePath, {
flags: "wx",
flush: true,
mode: 384
});
archiveWriteStream.once("open", (fileDescriptor) => {
try {
openedIdentity = fs.fstatSync(fileDescriptor);
} catch (error) {
archiveWriteStream.destroy(error);
}
});
try {
archiveStream = params.createArchiveStream(reportProgress);
const pipelinePromise = pipeline$1(archiveStream, progress, archiveWriteStream, { signal: controller.signal });
reportProgress();
await pipelinePromise;
const currentIdentity = await fs$1.lstat(params.archivePath);
if (!openedIdentity?.isFile() || !currentIdentity.isFile() || !sameFileIdentity(openedIdentity, currentIdentity)) throw new Error(`Backup archive path changed while writing: ${params.archivePath}`);
return {
archivePath: params.archivePath,
identity: currentIdentity
};
} catch (err) {
archiveWriteStream.destroy();
let cleanupReceipt = openedIdentity ? {
archivePath: params.archivePath,
identity: openedIdentity
} : void 0;
if (!cleanupReceipt) try {
const currentIdentity = fs.lstatSync(params.archivePath);
cleanupReceipt = currentIdentity.isFile() ? {
archivePath: params.archivePath,
identity: currentIdentity
} : { archivePath: params.archivePath };
} catch (cleanupError) {
if (cleanupError.code !== "ENOENT") cleanupReceipt = { archivePath: params.archivePath };
}
if (cleanupReceipt && (!cleanupReceipt.identity || !removePreparedBackupArchive(cleanupReceipt))) params.onPartialArchive?.(cleanupReceipt);
if (cleanupReceipt && !cleanupReceipt.identity) {
if (!params.onPartialArchive) try {
const currentIdentity = fs.lstatSync(cleanupReceipt.archivePath);
if (currentIdentity.isFile()) removePreparedBackupArchive({
archivePath: cleanupReceipt.archivePath,
identity: currentIdentity
});
} catch {}
}
throw idleTimeoutError ?? err;
} finally {
settled = true;
if (idleTimer) clearTimeout(idleTimer);
}
}
//#endregion
//#region src/infra/backup-archive-publication.ts
function pathsEqual(left, right) {
const resolvedLeft = path.resolve(left);
const resolvedRight = path.resolve(right);
return process.platform === "win32" ? resolvedLeft.toLowerCase() === resolvedRight.toLowerCase() : resolvedLeft === resolvedRight;
}
async function assertTargetAbsent(targetPath) {
try {
await fs$1.lstat(targetPath);
} catch (error) {
if (error.code === "ENOENT") return;
throw error;
}
throw new Error(`Refusing to overwrite existing backup archive: ${targetPath}`);
}
async function removeDirectoryIfOwned(directoryPath, expectedIdentity) {
const currentIdentity = await fs$1.lstat(directoryPath).catch(() => void 0);
if (!currentIdentity || !currentIdentity.isDirectory() || !sameFileIdentity(expectedIdentity, currentIdentity)) return false;
try {
await fs$1.rmdir(directoryPath);
return true;
} catch {
return false;
}
}
async function removeStagingDirectoryIfOwned(plan) {
return await removeDirectoryIfOwned(plan.stagingDir, plan.stagingIdentity);
}
async function createBackupArchivePublication(outputPath) {
const requestedOutputPath = path.resolve(outputPath);
const requestedParentPath = path.dirname(requestedOutputPath);
const canonicalParentPath = await fs$1.realpath(requestedParentPath);
const parentIdentity = await fs$1.lstat(canonicalParentPath);
if (!parentIdentity.isDirectory()) throw new Error(`Backup output parent is not a directory: ${requestedParentPath}`);
const canonicalOutputPath = path.join(canonicalParentPath, path.basename(requestedOutputPath));
await assertTargetAbsent(canonicalOutputPath);
const stagingDir = await fs$1.mkdtemp(path.join(canonicalParentPath, `.openclaw-backup-publish-${randomUUID()}-`));
let stagingIdentity;
try {
stagingIdentity = await fs$1.lstat(stagingDir);
await fs$1.chmod(stagingDir, 448);
return {
canonicalOutputPath,
canonicalParentPath,
parentReceipt: {
path: canonicalParentPath,
realPath: canonicalParentPath,
identity: parentIdentity
},
pendingCleanupArchives: [],
requestedOutputPath,
requestedParentPath,
stagingDir,
stagingIdentity,
tempArchivePath: path.join(stagingDir, "archive.tar.gz.tmp")
};
} catch (error) {
if (stagingIdentity) await removeDirectoryIfOwned(stagingDir, stagingIdentity);
throw error;
}
}
function retainArchiveForCleanup(plan, receipt) {
for (const [index, candidate] of plan.pendingCleanupArchives.entries()) {
if (!pathsEqual(candidate.archivePath, receipt.archivePath)) continue;
if (!candidate.identity || !receipt.identity) {
if (!candidate.identity && receipt.identity) plan.pendingCleanupArchives[index] = receipt;
return;
}
if (sameFileIdentity(candidate.identity, receipt.identity)) return;
}
plan.pendingCleanupArchives.push(receipt);
}
async function removePendingBackupArchive(plan, receipt) {
if (!pathsEqual(path.dirname(receipt.archivePath), plan.stagingDir)) return false;
if (receipt.identity) return removePreparedBackupArchive(receipt);
let currentIdentity;
try {
currentIdentity = await fs$1.lstat(receipt.archivePath);
} catch (error) {
return error.code === "ENOENT";
}
if (!currentIdentity.isFile()) return false;
return removePreparedBackupArchive({
archivePath: receipt.archivePath,
identity: currentIdentity
});
}
async function cleanupBackupArchivePublication(plan, log) {
const retainedArchives = plan.pendingCleanupArchives.splice(0);
for (const receipt of retainedArchives) if (!await removePendingBackupArchive(plan, receipt)) retainArchiveForCleanup(plan, receipt);
if (await removeStagingDirectoryIfOwned(plan)) {
await syncDirectoryIfSupported(plan.canonicalParentPath).catch(() => void 0);
return;
}
if (await fs$1.lstat(plan.stagingDir).catch(() => void 0)) log?.(`Backup archiver preserved changed or non-empty staging directory ${plan.stagingDir}.`);
}
async function publishPreparedBackupArchive(params) {
const { plan, prepared } = params;
let publicationPreserved = false;
let committed = false;
try {
try {
const publication = await publishFileExclusive({
sourcePath: prepared.archivePath,
targetPath: plan.canonicalOutputPath,
expectedSourceIdentity: prepared.identity,
parentReceipt: plan.parentReceipt,
strategy: "link-required",
onSyncFailure: "preserve"
});
publicationPreserved = true;
requireDirectorySync(publication.directorySync, "Backup publication directory");
committed = true;
} catch (error) {
const details = getPublishFileExclusiveFailureDetails(error);
publicationPreserved ||= details?.cleanup === "preserved";
if (error.code === "EEXIST") throw new Error(`Refusing to overwrite existing backup archive: ${plan.requestedOutputPath}`, { cause: error });
if (isHardlinkFallbackError(error)) throw new Error(`Atomic backup publication requires hard-link support in ${plan.requestedParentPath}.`, { cause: error });
if (error.code === "path-mismatch") throw new Error(`Backup archive changed during publication: ${plan.requestedOutputPath}`, { cause: error });
throw error;
}
if (!removePreparedBackupArchive(prepared)) {
retainArchiveForCleanup(plan, prepared);
params.log?.(`Backup archiver preserved changed staging file ${prepared.archivePath}.`);
}
if (!await removeStagingDirectoryIfOwned(plan)) params.log?.(`Backup archiver preserved changed or non-empty staging directory ${plan.stagingDir}.`);
await syncDirectoryIfSupported(plan.canonicalParentPath).catch((error) => {
params.log?.(`Backup archiver could not sync cleanup in ${plan.canonicalParentPath}: ${error.code ?? String(error)}.`);
});
} catch (error) {
if (!committed) {
if (publicationPreserved) params.log?.(`Backup archiver preserved the final archive after publication failed: ${plan.requestedOutputPath}.`);
if (!removePreparedBackupArchive(prepared)) retainArchiveForCleanup(plan, prepared);
await removeStagingDirectoryIfOwned(plan);
}
throw error;
}
}
//#endregion
//#region src/infra/state-migrations.audit-backup.ts
const LEGACY_AUDIT_LOGICAL_PATHS = [
{
directory: "logs",
basename: "config-audit.jsonl"
},
{
directory: "audit",
basename: "system-agent.jsonl"
},
{
directory: "audit",
basename: "crestodian.jsonl"
}
];
async function hasLegacyAuditBackupSources(stateDir) {
for (const logical of LEGACY_AUDIT_LOGICAL_PATHS) {
let entries;
try {
entries = await fs$1.readdir(path.join(stateDir, logical.directory));
} catch (error) {
if (error.code === "ENOENT") continue;
throw error;
}
const escaped = logical.basename.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
const sourcePattern = new RegExp(`^(?:${escaped}|\\.${escaped}\\.doctor-importing(?:\\.(?:[2-9]|[1-9][0-9]+))?|${escaped}\\.migrated(?:\\.(?:[2-9]|[1-9][0-9]+))?\\.raw(?:\\.doctor-scrub-(?:progress|restore|staging))?)$`, "u");
if (entries.some((entry) => sourcePattern.test(entry))) return true;
}
return false;
}
function isLegacyAuditMigrationBackupPath(sourcePath, stateDir) {
const relativePath = path.relative(path.resolve(stateDir), path.resolve(sourcePath));
if (!relativePath || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath)) return false;
const directory = path.dirname(relativePath);
const basename = path.basename(relativePath);
for (const logical of LEGACY_AUDIT_LOGICAL_PATHS) {
if (directory !== logical.directory) continue;
if (basename === logical.basename) return true;
const escaped = logical.basename.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
const claimPattern = new RegExp(`^\\.${escaped}\\.doctor-importing(?:\\.(?:[2-9]|[1-9][0-9]+))?$`, "u");
const rawPattern = new RegExp(`^${escaped}\\.migrated(?:\\.(?:[2-9]|[1-9][0-9]+))?\\.raw(?:\\.doctor-scrub-(?:progress|restore|staging))?$`, "u");
if (claimPattern.test(basename) || rawPattern.test(basename)) return true;
}
return false;
}
var LegacyAuditBackupStateChangedError = class extends Error {
constructor(message = "Legacy audit state changed while backup was capturing it") {
super(message);
this.name = "LegacyAuditBackupStateChangedError";
}
};
const LEGACY_AUDIT_RAW_CHECKPOINT_SCOPE = "migration.legacy-audit-raw";
function createLegacyAuditDatabaseWitness(database) {
const rows = database.prepare(`SELECT scope, event_key, payload_json, created_at, sequence
FROM diagnostic_events
WHERE (scope IN (?, ?) AND event_key GLOB 'legacy:*') OR scope = ?
ORDER BY scope, event_key, sequence`).all(CONFIG_AUDIT_SCOPE, SYSTEM_AGENT_AUDIT_SCOPE, LEGACY_AUDIT_RAW_CHECKPOINT_SCOPE);
return createHash("sha256").update(JSON.stringify(rows)).digest("hex");
}
async function readLegacyAuditDatabaseWitness(stateDir) {
return withExistingOpenClawStateDatabaseReadOnly(({ db }) => createLegacyAuditDatabaseWitness(db), { env: {
...process.env,
OPENCLAW_STATE_DIR: stateDir
} }) ?? createHash("sha256").digest("hex");
}
function legacyAuditBackupCapturesMatch(left, right) {
return left.filesystemWitness === right.filesystemWitness && left.databaseWitness === right.databaseWitness;
}
/** Replaces live raw checkpoints with metadata for the transformed backup files. */
function rewriteLegacyAuditBackupCheckpoints(database, snapshots) {
if (database.prepare("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = ?").get("diagnostic_events")?.ok !== 1) return;
const scope = "migration.legacy-audit-raw";
database.prepare("DELETE FROM diagnostic_events WHERE scope = ?").run(scope);
const insert = database.prepare(`INSERT INTO diagnostic_events (
scope, event_key, payload_json, created_at, sequence
) VALUES (?, ?, ?, ?, ?)`);
let sequence = 1;
for (const snapshot of snapshots) {
if (!snapshot.checkpoint) continue;
insert.run(scope, snapshot.checkpoint.key, JSON.stringify(snapshot.checkpoint.value), 0, sequence);
sequence += 1;
}
}
async function createLegacyAuditBackupSnapshotsOnce(params) {
const detected = detectLegacyAuditLogs({
stateDir: params.stateDir,
doctorOnlyStateMigrations: true
});
if (detected.sources.length === 0) return {
snapshots: [],
filesystemWitness: createHash("sha256").digest("hex")
};
const root$1 = await root(params.stateDir, {
hardlinks: "reject",
maxBytes: Number.MAX_SAFE_INTEGER,
mkdir: false,
mode: 384,
symlinks: "reject"
});
const snapshots = [];
const filesystemWitness = createHash("sha256");
for (const [index, source] of detected.sources.entries()) {
const sourceRelativePath = path.relative(path.resolve(params.stateDir), source.sourcePath);
const snapshot = source.storage === "raw-archive" ? await readLegacyAuditRecoverySourceForBackup(root$1, sourceRelativePath) : await readLegacyAuditSourcePrefixSnapshotForBackup(root$1, sourceRelativePath);
const sourceGeneration = legacyAuditSourceGenerationKey(sourceRelativePath);
const previousCheckpoint = source.storage === "raw-archive" ? findPreviousLegacyAuditRawCheckpoint(params.stateDir, sourceRelativePath) : void 0;
const prepared = prepareLegacyAuditRecords(source, snapshot.raw, sourceGeneration, previousCheckpoint?.recordOrdinalBase ?? 0);
if (!prepared.ok) throw new Error(`Legacy ${source.label} append archive cannot be sanitized for backup: ${prepared.warnings.join("; ")}`);
const sourcePath = path.join(params.tempDir, `legacy-audit-raw-${index}.jsonl`);
await fs$1.writeFile(sourcePath, prepared.sanitizedJsonl, { mode: 384 });
let checkpoint;
if (previousCheckpoint) {
if (previousCheckpoint.recordCount > prepared.records.length) throw new Error(`Legacy ${source.label} append archive is shorter than its durable checkpoint`);
const transformedPrefix = Buffer.from(serializePreparedAuditRecords(prepared.records.slice(0, previousCheckpoint.recordCount)), "utf8");
const value = {
...previousCheckpoint,
dev: 0,
ino: 0,
mtimeMs: 0,
size: transformedPrefix.length,
contentHash: createHash("sha256").update(transformedPrefix).digest("hex")
};
checkpoint = {
key: legacyAuditRawCheckpointKey(value),
value
};
}
const backupSnapshot = {
sourcePath,
archiveSourcePath: source.sourcePath,
...checkpoint ? { checkpoint } : {},
skippedSourcePaths: /* @__PURE__ */ new Set([
path.resolve(source.sourcePath),
path.resolve(`${source.sourcePath}.doctor-scrub-progress`),
path.resolve(`${source.sourcePath}.doctor-scrub-restore`),
path.resolve(`${source.sourcePath}.doctor-scrub-staging`)
])
};
const witnessMetadata = JSON.stringify([
backupSnapshot.archiveSourcePath,
snapshot.dev,
snapshot.ino,
snapshot.mtimeMs,
snapshot.size,
backupSnapshot.checkpoint
]);
for (const value of [witnessMetadata, prepared.sanitizedJsonl]) filesystemWitness.update(String(Buffer.byteLength(value))).update(":").update(value);
snapshots.push(backupSnapshot);
}
return {
snapshots,
filesystemWitness: filesystemWitness.digest("hex")
};
}
async function createLegacyAuditBackupCapture(params) {
const capture = await createLegacyAuditBackupSnapshots(params);
const databaseWitness = await readLegacyAuditDatabaseWitness(params.stateDir);
return {
...capture,
databaseWitness
};
}
async function createLegacyAuditBackupSnapshots(params) {
let lastError;
for (let attempt = 0; attempt < 3; attempt += 1) try {
return await createLegacyAuditBackupSnapshotsOnce(params);
} catch (error) {
lastError = error;
if (attempt < 2) await new Promise((resolve) => {
setTimeout(resolve, 25);
});
}
throw lastError;
}
//#endregion
//#region src/infra/backup-sqlite-snapshot.ts
function findLegacyAuditBackupStateChange(error) {
return collectErrorGraphCandidates(error, (candidate) => candidate instanceof Error ? [candidate.cause] : []).find((candidate) => candidate instanceof LegacyAuditBackupStateChangedError);
}
function resolveBackupAgentDatabaseOwner(sourcePath, inventory) {
const resolvedSourcePath = path.resolve(sourcePath);
if (path.basename(resolvedSourcePath) !== "openclaw-agent.sqlite") return;
const stateSegments = path.relative(inventory.stateDir, resolvedSourcePath).split(path.sep);
const defaultLayoutAgentId = stateSegments.length === 4 && stateSegments[0] === "agents" && stateSegments[2] === "agent" ? stateSegments[1] : void 0;
if (defaultLayoutAgentId && normalizeAgentId(defaultLayoutAgentId) !== defaultLayoutAgentId) throw new Error(`Canonical agent SQLite path has a noncanonical agent owner ${defaultLayoutAgentId}: ${resolvedSourcePath}`);
const declaredOwners = inventory.agentRoots.filter(({ databasePath }) => path.resolve(databasePath) === resolvedSourcePath);
if (declaredOwners.length > 1) {
const distinctAgentIds = new Set(declaredOwners.map(({ agentId }) => agentId));
if (distinctAgentIds.size > 1) throw new Error(`Canonical agent SQLite path has multiple configured owners (${[...distinctAgentIds].join(", ")}): ${resolvedSourcePath}`);
}
const configuredAgentId = declaredOwners[0]?.agentId;
if (configuredAgentId) return configuredAgentId;
return defaultLayoutAgentId;
}
function resolveSqliteBackupDatabasePath(sourcePath) {
for (const suffix of SQLITE_SIDECAR_SUFFIXES) if (sourcePath.endsWith(suffix)) {
const databasePath = sourcePath.slice(0, -suffix.length);
return databasePath.endsWith(".sqlite") ? databasePath : void 0;
}
return sourcePath.endsWith(".sqlite") ? sourcePath : void 0;
}
function classifyBackupSqliteSource(sourcePath, inventory) {
const resolvedSourcePath = path.resolve(sourcePath);
const transient = isTransientSqliteBackupPath(resolvedSourcePath);
const databasePath = resolveSqliteBackupDatabasePath(resolvedSourcePath);
if (!transient && !databasePath) return;
if (!(isPathWithin(resolvedSourcePath, inventory.stateDir) || inventory.agentRoots.some(({ sourcePath: agentRoot }) => isPathWithin(resolvedSourcePath, agentRoot))) || inventory.isPackageContent(resolvedSourcePath)) return;
if (transient) return "excluded";
return inventory.isIncluded(resolvedSourcePath) ? "sqlite" : "excluded";
}
async function discoverBackupSqliteSources(params) {
const snapshotPaths = /* @__PURE__ */ new Set();
const discoveredSourcePaths = /* @__PURE__ */ new Set();
const visitedDirectories = /* @__PURE__ */ new Set();
const gatewayLockDir = resolveGatewayLockDir(params.inventory.stateDir);
async function visit(directoryPath) {
const resolvedDirectoryPath = path.resolve(directoryPath);
if (visitedDirectories.has(resolvedDirectoryPath)) return;
visitedDirectories.add(resolvedDirectoryPath);
let entries;
try {
entries = await fs$1.readdir(resolvedDirectoryPath, { withFileTypes: true });
} catch (error) {
if (hasErrnoCode(error, "ENOENT")) return;
throw error;
}
for (const entry of entries) {
const entryPath = path.join(resolvedDirectoryPath, entry.name);
if (isPathWithin(entryPath, gatewayLockDir) || params.inventory.isVolatile(entryPath)) continue;
if (entry.isDirectory()) {
if (params.inventory.isTraversable(entryPath) && !params.inventory.isPackageContent(entryPath)) await visit(entryPath);
continue;
}
if (!params.inventory.isIncluded(entryPath)) continue;
if (entry.isSymbolicLink()) {
if (resolveBackupAgentDatabaseOwner(entryPath, params.inventory)) {
let targetEntry;
try {
targetEntry = await fs$1.stat(entryPath);
} catch (error) {
throw new Error(`Canonical agent SQLite symlink cannot be snapshotted: ${entryPath}`, { cause: error });
}
if (!targetEntry.isFile()) throw new Error(`Canonical agent SQLite symlink must resolve to a regular file: ${entryPath}`);
snapshotPaths.add(entryPath);
discoveredSourcePaths.add(entryPath);
}
continue;
}
if (!entry.isFile() || classifyBackupSqliteSource(entryPath, params.inventory) !== "sqlite") continue;
discoveredSourcePaths.add(entryPath);
if (entry.name.endsWith(".sqlite")) snapshotPaths.add(entryPath);
}
}
await visit(params.inventory.stateDir);
for (const { sourcePath } of params.inventory.agentRoots) await visit(sourcePath);
const globalStateSqlitePath = path.resolve(params.globalStateSqlitePath);
let globalStateEntry;
try {
globalStateEntry = await fs$1.lstat(globalStateSqlitePath);
} catch (error) {
if (!hasErrnoCode(error, "ENOENT")) throw error;
}
if (globalStateEntry?.isFile()) {
snapshotPaths.add(globalStateSqlitePath);
discoveredSourcePaths.add(globalStateSqlitePath);
} else if (globalStateEntry?.isSymbolicLink()) {
let targetEntry;
try {
targetEntry = await fs$1.stat(globalStateSqlitePath);
} catch (error) {
throw new Error(`Canonical global SQLite symlink cannot be snapshotted: ${globalStateSqlitePath}`, { cause: error });
}
if (!targetEntry.isFile()) throw new Error(`Canonical global SQLite symlink must resolve to a regular file: ${globalStateSqlitePath}`);
snapshotPaths.add(globalStateSqlitePath);
discoveredSourcePaths.add(globalStateSqlitePath);
} else if (globalStateEntry) throw new Error(`Canonical global SQLite path must be a regular file or symlink to one: ${globalStateSqlitePath}`);
return {
snapshotPaths: [...snapshotPaths].toSorted((left, right) => left.localeCompare(right)),
discoveredSourcePaths
};
}
async function createBackupSqliteSnapshotPlan(params) {
const globalStateSqlitePath = path.resolve(resolveOpenClawStateSqlitePath({
...process.env,
OPENCLAW_STATE_DIR: params.inventory.stateDir
}));
const discovery = await discoverBackupSqliteSources({
inventory: params.inventory,
globalStateSqlitePath
});
const globalStateIdentity = await fs$1.stat(globalStateSqlitePath).catch((error) => {
if (hasErrnoCode(error, "ENOENT")) return;
throw error;
});
const canonicalSources = [];
if (globalStateIdentity) canonicalSources.push({
role: "global",
archiveSourcePath: globalStateSqlitePath,
identity: globalStateIdentity,
sourcePath: await fs$1.realpath(globalStateSqlitePath)
});
for (const archiveSourcePath of discovery.snapshotPaths) {
const agentId = resolveBackupAgentDatabaseOwner(archiveSourcePath, params.inventory);
if (!agentId) continue;
if (normalizeAgentId(agentId) !== agentId) throw new Error(`Canonical agent SQLite path has a noncanonical agent owner ${agentId}: ${archiveSourcePath}`);
canonicalSources.push({
role: "agent",
agentId,
archiveSourcePath,
identity: await fs$1.stat(archiveSourcePath),
sourcePath: await fs$1.realpath(archiveSourcePath)
});
}
const snapshots = [];
for (const archiveSourcePath of discovery.snapshotPaths) {
const archiveSourceIdentity = await fs$1.stat(archiveSourcePath);
const exactCanonicalSource = canonicalSources.find((source) => path.resolve(source.archiveSourcePath) === path.resolve(archiveSourcePath));
if (exactCanonicalSource && !sameFileIdentity(exactCanonicalSource.identity, archiveSourceIdentity)) throw new Error(`Canonical SQLite path changed after discovery: ${archiveSourcePath}`);
const matchingCanonicalSources = exactCanonicalSource ? [exactCanonicalSource] : canonicalSources.filter((source) => sameFileIdentity(source.identity, archiveSourceIdentity));
if (matchingCanonicalSources.length > 1) {
const owners = matchingCanonicalSources.map((source) => source.role === "global" ? "global" : `agent:${source.agentId}`).join(", ");
throw new Error(`SQLite path aliases multiple canonical database owners (${owners}): ${archiveSourcePath}`);
}
const canonicalSource = matchingCanonicalSources[0];
const sourceDatabasePath = canonicalSource?.sourcePath ?? archiveSourcePath;
const sourcePath = path.join(params.tempDir, `openclaw-state-db-${snapshots.length}.sqlite`);
try {
await createVerifiedSqliteSnapshot({
sourcePath: sourceDatabasePath,
targetPath: sourcePath,
requireNonEmptySource: Boolean(canonicalSource),
validate: canonicalSource?.role === "global" ? (database, pathname) => assertOpenClawStateDatabaseOwner(database, { pathname }) : canonicalSource?.role === "agent" ? (database, pathname) => assertOpenClawAgentDatabaseOwner(database, {
agentId: canonicalSource.agentId,
pathname
}) : void 0,
transform: canonicalSource?.role === "global" ? (database) => {
if (params.legacyAuditDatabaseWitness !== void 0 && createLegacyAuditDatabaseWitness(database) !== params.legacyAuditDatabaseWitness) throw new LegacyAuditBackupStateChangedError("Legacy audit database rows changed during SQLite backup");
sanitizeOpenClawGlobalStateSnapshot(database);
rewriteLegacyAuditBackupCheckpoints(database, params.legacyAuditSnapshots);
} : canonicalSource?.role === "agent" ? sanitizeOpenClawStateLeaseRows : void 0
});
} catch (error) {
const stateChange = findLegacyAuditBackupStateChange(error);
if (stateChange) throw stateChange;
throw new Error(`SQLite database cannot be compacted safely for backup: ${archiveSourcePath}. ${formatErrorMessage(error)}. The source must pass full integrity checks, online SQLite backup, and offline compaction with its required SQLite capabilities; a direct file copy was refused because it can retain deleted data.`, { cause: error });
}
snapshots.push({
sourcePath,
archiveSourcePath,
skippedSourcePaths: new Set([archiveSourcePath, sourceDatabasePath].flatMap((databasePath) => resolveSqliteDatabaseFilePaths(databasePath).map((pathname) => path.resolve(pathname))))
});
}
return {
snapshots,
discoveredSourcePaths: discovery.discoveredSourcePaths
};
}
//#endregion
//#region src/infra/backup-tar-retry.ts
const BACKUP_TAR_MAX_ATTEMPTS = 3;
const BACKUP_TAR_BACKOFF_MS = [1e4, 2e4];
function isTarEofRaceError(err) {
if (!err || typeof err !== "object") return false;
if (err.code === "EOF") return true;
const message = err.message ?? "";
return /(did not encounter expected|encountered unexpected) EOF|TAR_BAD_ARCHIVE/i.test(message);
}
function resolveBackupTarAttemptTempPath(tempArchivePath, attempt) {
return attempt === 1 ? tempArchivePath : `${tempArchivePath}.retry-${attempt}`;
}
async function writeTarArchiveWithRetry(params) {
const sleepFn = params.sleepMs ?? sleep;
let lastErr;
let attempts = 0;
for (let attempt = 1; attempt <= BACKUP_TAR_MAX_ATTEMPTS; attempt += 1) {
attempts = attempt;
const attemptTempArchivePath = resolveBackupTarAttemptTempPath(params.tempArchivePath, attempt);
try {
return await params.runTar(attemptTempArchivePath);
} catch (err) {
lastErr = err;
if (!isTarEofRaceError(err) || attempt === BACKUP_TAR_MAX_ATTEMPTS) break;
const backoff = BACKUP_TAR_BACKOFF_MS[attempt - 1] ?? 0;
const offendingPath = err.path;
params.log?.(`Backup archiver hit a live-write race${offendingPath ? ` on ${offendingPath}` : ""} (attempt ${attempt}/${BACKUP_TAR_MAX_ATTEMPTS}); retrying in ${Math.round(backoff / 1e3)}s.`);
await sleepFn(backoff);
}
}
const final = lastErr instanceof Error ? lastErr : new Error(String(lastErr));
const offendingPath = lastErr?.path;
const attemptSuffix = `after ${attempts} attempt${attempts === 1 ? "" : "s"}`;
const suffix = offendingPath ? ` (last offending path: ${offendingPath}, ${attemptSuffix})` : ` (${attemptSuffix})`;
throw new Error(`Backup archive write failed: ${final.message}${suffix}`, { cause: final });
}
//#endregion
//#region src/infra/backup-create.ts
const loadTarRuntime = createLazyRuntimeModule(() => import("tar"));
async function resolveOutputPath(params) {
const basename = buildBackupArchiveBasename(params.nowMs);
const rawOutput = params.output?.trim();
if (!rawOutput) {
const cwd = path.resolve(process.cwd());
const canonicalCwd = await fs$1.realpath(cwd).catch(() => cwd);
const defaultDir = params.includedAssets.some((asset) => isPathWithin(canonicalCwd, asset.sourcePath)) ? resolveHomeDir() ?? path.dirname(params.stateDir) : cwd;
return path.resolve(defaultDir, basename);
}
const resolved = resolveUserPath(rawOutput);
if (rawOutput.endsWith("/") || rawOutput.endsWith("\\")) return path.join(resolved, basename);
try {
if ((await fs$1.stat(resolved)).isDirectory()) return path.join(resolved, basename);
} catch {}
return resolved;
}
function formatBackupOutputFailure(error, outputPath, phase, ownedRoot) {
const cause = phase === "write" && error instanceof Error ? error.cause : void 0;
const filesystemError = isErrno(error) ? error : isErrno(cause) ? cause : null;
if (!filesystemError) return error;
if (ownedRoot) {
const failedPath = filesystemError.path;
if (typeof failedPath !== "string" || !isPathWithin(path.resolve(failedPath), ownedRoot)) return error;
}
const outputParent = path.dirname(outputPath);
const retry = "run `openclaw backup create --output <archive>` again.";
let detail;
switch (filesystemError.code) {
case "ENOENT":
detail = `Backup output directory could not be created: ${outputParent}. Check the path and ${retry}`;
break;
case "EACCES":
case "EPERM":
case "EROFS":
detail = `Backup output directory is not writable: ${outputParent}. Check the path and directory permissions, then ${retry}`;
break;
case "EEXIST":
case "ENOTDIR":
if (phase !== "parent") return error;
detail = `Backup output parent is not a directory: ${outputParent}. Choose a directory path and ${retry}`;
break;
case "ENOSPC":
detail = `The destination does not have enough free space: ${outputParent}. Free up disk space and ${retry}`;
break;
case "EDQUOT":
detail = `The destination storage quota is exhausted: ${outputParent}. Free up space or choose another path, then ${retry}`;
break;
default: detail = `The output path could not be prepared: ${outputParent}. Check the path and filesystem, then ${retry}`;
}
return new Error(`Backup archive creation failed: ${outputPath}. ${detail}`, { cause: error });
}
async function assertOutputPathReady(outputPath) {
try {
await fs$1.access(outputPath);
throw new Error(`Refusing to overwrite existing backup archive: ${outputPath}`);
} catch (error) {
const code = error?.code;
if (code === "ENOENT" || code === "ENOTDIR") return;
throw formatBackupOutputFailure(error, outputPath, "parent");
}
}
async function prepareBackupOutputParent(outputPath) {
try {
await fs$1.mkdir(path.dirname(outputPath), { recursive: true });
} catch (error) {
throw formatBackupOutputFailure(error, outputPath, "parent");
}
}
async function chooseBackupTempRoot(params) {
const systemTmp = os.tmpdir();
const canonicalSystemTmp = await canonicalizePathForContainment(systemTmp);
if (!params.assets.some((asset) => isPathWithin(canonicalSystemTmp, asset.sourcePath))) return systemTmp;
const fallback = path.dirname(params.outputPath);
const canonicalFallback = await canonicalizePathForContainment(fallback);
const fallbackInsideAsset = params.assets.find((asset) => isPathWithin(canonicalFallback, asset.sourcePath));
if (fallbackInsideAsset) throw new Error(`Backup temp root cannot be placed outside every source path: ${systemTmp} and ${fallback} both overlap ${fallbackInsideAsset.sourcePath}.`);
return fallback;
}
function buildManifest(result, plan) {
return {
schemaVersion: 1,
createdAt: result.createdAt,
archiveRoot: result.archiveRoot,
runtimeVersion: resolveRuntimeServiceVersion(),
platform: process.platform,
nodeVersion: process.version,
options: {
includeWorkspace: result.includeWorkspace,
onlyConfig: result.onlyConfig
},
paths: {
stateDir: plan.stateDir,
configPath: plan.configPath,
oauthDir: plan.oauthDir,
workspaceDirs: plan.workspaceDirs,
...result.agentRoots ? { agentRoots: [...result.agentRoots] } : {}
},
assets: result.assets.map((asset) => ({
kind: asset.kind,
sourcePath: asset.sourcePath,
archivePath: asset.archivePath
})),
skipped: result.skipped.map((entry) => ({
kind: entry.kind,
sourcePath: entry.sourcePath,
reason: entry.reason,
coveredBy: entry.coveredBy
}))
};
}
function formatBackupCreateSummary(result) {
const lines = [`Backup archive: ${result.archivePath}`];
lines.push(`Included ${result.assets.length} path${result.assets.length === 1 ? "" : "s"}:`);
for (const asset of result.assets) lines.push(`- ${asset.kind}: ${asset.displayPath}`);
if (result.skipped.length > 0) {
lines.push(`Skipped ${result.skipped.length} path${result.skipped.length === 1 ? "" : "s"}:`);
for (const entry of result.skipped) if (entry.reason === "covered" && entry.coveredBy) lines.push(`- ${entry.kind}: ${entry.displayPath} (${entry.reason} by ${entry.coveredBy})`);
else lines.push(`- ${entry.kind}: ${entry.displayPath} (${entry.reason})`);
}
if (result.dryRun) lines.push("Dry run only; archive was not written.");
else {
lines.push(`Created ${result.archivePath}`);
if (result.skippedVolatileCount > 0) lines.push(`Skipped ${result.skippedVolatileCount} volatile file${result.skippedVolatileCount === 1 ? "" : "s"} (live sessions, cron logs, queues, managed runtime paths, sockets, pid/tmp).`);
if (result.verified) lines.push("Archive verification: passed");
}
return lines;
}
function remapArchiveEntryPath(params) {
const normalizedEntry = path.resolve(params.entryPath);
if (normalizedEntry === params.manifestPath) return path.posix.join(params.archiveRoot, "manifest.json");
const remappedSourcePath = params.sourcePathRemaps?.get(normalizedEntry);
if (remappedSourcePath) return buildBackupArchivePath(params.archiveRoot, remappedSourcePath);
return buildBackupArchivePath(params.archiveRoot, normalizedEntry);
}
function remapDeclaredAbsoluteSymbolicLinkTarget(params) {
if (!params.linkpath || !path.isAbsolute(params.linkpath) || params.linkpath.includes("\\")) return params.linkpath;
let targetSourcePath;
try {
targetSourcePath = realpathSync(params.linkpath);
} catch {
return params.linkpath;
}
if (!params.assets.some((asset) => isPathWithin(targetSourcePath, asset.sourcePath))) return params.linkpath;
return path.posix.relative(path.posix.dirname(params.archiveEntryPath), buildBackupArchivePath(params.archiveRoot, targetSourcePath));
}
function isBackupTarFilterFile(entry) {
return "isFile" in entry ? entry.isFile() : entry.type === "File";
}
const MAX_LEGACY_AUDIT_CAPTURE_ATTEMPTS = 3;
async function createConsistentStateSnapshotPlan(params) {
if (params.onlyConfig) return {
legacyAuditSnapshots: [],
stateSqliteBackup: {
snapshots: [],
discoveredSourcePaths: /* @__PURE__ */ new Set()
}
};
if (!params.stateDir) return {
legacyAuditSnapshots: [],
stateSqliteBackup: await createBackupSqliteSnapshotPlan({
inventory: params.inventory,
tempDir: params.tempDir,
legacyAuditSnapshots: []
})
};
const stateDir = params.stateDir;
if (!await hasLegacyAuditBackupSources(stateDir)) {
const fastAttemptDir = path.join(params.tempDir, "state-snapshot-no-legacy");
await fs$1.mkdir(fastAttemptDir, { recursive: true });
const stateSqliteBackup = await createBackupSqliteSnapshotPlan({
inventory: params.inventory,
tempDir: fastAttemptDir,
legacyAuditSnapshots: []
});
if (!await hasLegacyAuditBackupSources(stateDir)) return {
legacyAuditSnapshots: [],
stateSqliteBackup
};
await fs$1.rm(fastAttemptDir, {
recursive: true,
force: true
});
}
let lastStateChangeMessage;
for (let attempt = 0; attempt < MAX_LEGACY_AUDIT_CAPTURE_ATTEMPTS; attempt += 1) {
const attemptDir = path.join(params.tempDir, `state-snapshot-attempt-${attempt + 1}`);
const verificationDir = path.join(attemptDir, "legacy-verification");
await fs$1.mkdir(attemptDir, { recursive: true });
try {
const firstCapture = await withLegacyAuditMigrationLease(stateDir, () => createLegacyAuditBackupCapture({
stateDir,
tempDir: attemptDir
}));
const stateSqliteBackup = await createBackupSqliteSnapshotPlan({
inventory: params.inventory,
tempDir: attemptDir,
legacyAuditSnapshots: firstCapture.snapshots,
legacyAuditDatabaseWitness: firstCapture.databaseWitness
});
await fs$1.mkdir(verificationDir, { recursive: true });
if (!legacyAuditBackupCapturesMatch(firstCapture, await withLegacyAuditMigrationLease(stateDir, () => createLegacyAuditBackupCapture({
stateDir,
tempDir: verificationDir
})))) throw new LegacyAuditBackupStateChangedError();
await fs$1.rm(verificationDir, {
recursive: true,
force: true
});
return {
legacyAuditSnapshots: firstCapture.snapshots,
stateSqliteBackup
};
} catch (error) {
await fs$1.rm(attemptDir, {
recursive: true,
force: true
});
if (!(error instanceof LegacyAuditBackupStateChangedError)) throw error;
lastStateChangeMessage = error.message;
}
}
throw new LegacyAuditBackupStateChangedError(`${lastStateChangeMessage ?? "Legacy audit state changed while backup was capturing it"}; retry backup after legacy audit migration settles`);
}
async function createBackupArchive(opts = {}) {
const nowMs = resolveDateTimestampMs(opts.nowMs);
const archiveRoot = buildBackupArchiveRoot(nowMs);
const onlyConfig = Boolean(opts.onlyConfig);
const includeWorkspace = onlyConfig ? false : opts.includeWorkspace ?? true;
const plan = await resolveBackupPlanFromDisk({
includeWorkspace,
onlyConfig,
nowMs
});
const outputPath = await resolveOutputPath({
output: opts.output,
nowMs,
includedAssets: plan.included,
stateDir: plan.stateDir
});
if (plan.included.length === 0) throw new Error(onlyConfig ? "No OpenClaw config file was found to back up." : "No local OpenClaw state was found to back up.");
const canonicalOutputPath = await canonicalizePathForContainment(outputPath);
const overlappingAsset = plan.included.find((asset) => isPathWithin(canonicalOutputPath, asset.sourcePath));
if (overlappingAsset) throw new Error(`Backup output must not be written inside a source path: ${outputPath} is inside ${overlappingAsset.sourcePath}`);
if (!opts.dryRun) await assertOutputPathReady(outputPath);
const createdAt = new Date(nowMs).toISOString();
const stateAsset = plan.included.find((asset) => asset.kind === "state");
const result = {
createdAt,
archiveRoot,
archivePath: outputPath,
dryRun: Boolean(opts.dryRun),
includeWorkspace,
onlyConfig,
verified: false,
assets: plan.included,
...onlyConfig ? {} : { agentRoots: plan.inventory.agentRoots.map(({ agentId, sourcePath }) => ({
agentId,
sourcePath
})) },
skipped: plan.skipped,
skippedVolatileCount: 0
};
if (opts.dryRun) return result;
await prepareBackupOutputParent(outputPath);
const tempRoot = await chooseBackupTempRoot({
assets: result.assets,
outputPath
});
await fs$1.mkdir(tempRoot, { recursive: true });
const tempDir = await fs$1.mkdtemp(path.join(tempRoot, "openclaw-backup-"));
const manifestPath = path.join(tempDir, "manifest.json");
let publication;
try {
publication = await createBackupArchivePublication(outputPath);
} catch (error) {
await fs$1.rm(tempDir, {
recursive: true,
force: true
}).catch(() => void 0);
throw formatBackupOutputFailure(error, outputPath, "publication");
}
const tempArchivePath = publication.tempArchivePath;
try {
const { legacyAuditSnapshots, stateSqliteBackup } = await createConsistentStateSnapshotPlan({
inventory: plan.inventory,
stateDir: stateAsset?.sourcePath,
tempDir,
onlyConfig
});
const sourcePathRemaps = /* @__PURE__ */ new Map();
const skippedStateSourcePaths = /* @__PURE__ */ new Set();
for (const snapshot of stateSqliteBackup.snapshots) {
sourcePathRemaps.set(path.resolve(snapshot.sourcePath), snapshot.archiveSourcePath);
for (const skippedSourcePath of snapshot.skippedSourcePaths) skippedStateSourcePaths.add(skippedSourcePath);
}
for (const snapshot of legacyAuditSnapshots) {
sourcePathRemaps.set(path.resolve(snapshot.sourcePath), snapshot.archiveSourcePath);
for (const skippedSourcePath of snapshot.skippedSourcePaths) skippedStateSourcePaths.add(skippedSourcePath);
}
const manifest = buildManifest(result, plan);
await writeJson(manifestPath, manifest, { trailingNewline: true });
const tar = await loadTarRuntime();
const gatewayLockDir = resolveGatewayLockDir(plan.stateDir);
let skippedVolatileCount = 0;
const unexpectedSqliteSourcePaths = [];
let archiveSymlinkViolation;
const tarFilter = (entryPath, entryStat) => {
const resolvedEntryPath = path.resolve(entryPath);
if (resolvedEntryPath === manifestPath) return true;
const isDirectory = "isDirectory" in entryStat ? entryStat.isDirectory() : entryStat.type === "Directory";
if (!onlyConfig && !(isDirectory ? plan.inventory.isTraversable(resolvedEntryPath) : plan.inventory.isIncluded(resolvedEntryPath))) return false;
if (isPathWithin(resolvedEntryPath, gatewayLockDir)) return false;
if (stateAsset && isLegacyAuditMigrationBackupPath(resolvedEntryPath, stateAsset.sourcePath)) return false;
const sqliteSourceKind = onlyConfig ? void 0 : classifyBackupSqliteSource(resolvedEntryPath, plan.inventory);
if (sqliteSourceKind === "excluded") return false;
if (skippedStateSourcePaths.has(resolvedEntryPath)) return false;
if (sqliteSourceKind === "sqlite" && stateSqliteBackup.discoveredSourcePaths.has(resolvedEntryPath)) return false;
if (sqliteSourceKind === "sqlite" && isBackupTarFilterFile(entryStat)) {
unexpectedSqliteSourcePaths.push(entryPath);
return false;
}
if (plan.inventory.isVolatile(resolvedEntryPath)) {
skippedVolatileCount += 1;
return false;
}
return true;
};
const completedArchive = await writeTarArchiveWithRetry({
tempArchivePath,
log: opts.log,
runTar: async (attemptTempArchivePath) => {
skippedVolatileCount = 0;
unexpectedSqliteSourcePaths.length = 0;
archiveSymlinkViolation = void 0;
const prepared = await writeArchiveStreamToFile({
archivePath: attemptTempArchivePath,
createArchiveStream: (reportProgress) => tar.c({
gzip: true,
portable: true,
preservePaths: true,
linkCache: createBackupLinkCache(),
statCache: createBackupVolatileStatCache(plan.inventory.isVolatile),
filter: (entryPath, entryStat) => {
reportProgress({
phase: "traversal",
entryPath
});
return tarFilter(entryPath, entryStat);
},
onWriteEntry: (entry) => {
const sourceEntryPath = entry.path;
reportProgress({
phase: "entry",
entryPath: sourceEntryPath
});
if (entry.type === "File" && (entry.stat?.size ?? 0) > 0) observeBackupTarEntryProgress(entry, (bytes) => {
reportProgress({
phase: "raw",
entryPath: sourceEntryPath,
bytes
});
});
const archiveEntryPath = remapArchiveEntryPath({
entryPath: entry.path,
manifestPath,
archiveRoot,
sourcePathRemaps
});
if (entry.type === "SymbolicLink" && !archiveSymlinkViolation) try {
entry.linkpath = remapDeclaredAbsoluteSymbolicLinkTarget({
linkpath: entry.linkpath,
archiveEntryPath,
archiveRoot,
assets: result.assets
});
assertArchiveSymbolicLinkTarget({
archiveRoot,
entryPath: archiveEntryPath,
linkpath: entry.linkpath,
assets: manifest.assets
});
} catch (error) {
archiveSymlinkViolation = error instanceof Error ? error : new Error(String(error));
}
entry.path = archiveEntryPath;
}
}, [
manifestPath,
...stateSqliteBackup.snapshots.map((snapshot) => snapshot.sourcePath),
...legacyAuditSnapshots.map((snapshot) => snapshot.sourcePath),
...result.assets.map((asset) => asset.sourcePath)
]),
onPartialArchive: (partialArchive) => {
publication.pendingCleanupArchives.push(partialArchive);
}
});
const unexpectedSqliteSourcePath = unexpectedSqliteSourcePaths[0];
const archiveValidationError = unexpectedSqliteSourcePath ? /* @__PURE__ */ new Error(`SQLite state appeared after snapshot discovery: ${unexpectedSqliteSourcePath}. Retry backup so it can be snapshotted.`) : archiveSymlinkViolation;
if (archiveValidationError) {
if (!removePreparedBackupArchive(prepared)) publication.pendingCleanupArchives.push(prepared);
throw archiveValidationError;
}
return prepared;
}
}).catch((error) => {
throw formatBackupOutputFailure(error, outputPath, "write", publication.stagingDir);
});
result.skippedVolatileCount = skippedVolatileCount;
if (skippedVolatileCount > 0) opts.log?.(`Backup skipped ${skippedVolatileCount} volatile file${skippedVolatileCount === 1 ? "" : "s"} (live sessions, cron logs, queues, managed runtime paths, sockets, pid/tmp).`);
try {
await publishPreparedBackupArchive({
plan: publication,
prepared: completedArchive,
log: opts.log
});
} catch (error) {
throw formatBackupOutputFailure(error, outputPath, "publication");
}
} finally {
await cleanupBackupArchivePublication(publication, opts.log);
await fs$1.rm(tempDir, {
recursive: true,
force: true
}).catch(() => void 0);
}
return result;
}
//#endregion
//#region src/commands/backup.ts
const backupVerifyRuntimeLoader = createLazyImportLoader(() => import("./backup-verify-lB8v7C_b.js"));
function loadBackupVerifyRuntime() {
return backupVerifyRuntimeLoader.load();
}
/** Create a backup archive, optionally verify it, and emit text or JSON output. */
async function backupCreateCommand(runtime, opts = {}) {
let archivePath = opts.output ?? process.cwd();
try {
const result = await createBackupArchive({
...opts,
log: opts.log ?? (opts.json ? void 0 : (message) => runtime.log(message))
});
archivePath = result.archivePath;
if (opts.verify && !opts.dryRun) {
const { backupVerifyCommand } = await loadBackupVerifyRuntime();
await backupVerifyCommand({
...runtime,
log: () => {}
}, {
archive: result.archivePath,
json: false
});
result.verified = true;
}
if (!opts.dryRun) recordBackupOutcomeBestEffort(runtime, {
kind: "archive",
archivePath,
status: "ok"
});
if (opts.json) writeRuntimeJson(runtime, result);
else runtime.log(formatBackupCreateSummary(result).join("\n"));
return result;
} catch (error) {
if (!opts.dryRun) recordBackupOutcomeBestEffort(runtime, {
kind: "archive",
archivePath,
status: "failed",
error: formatErrorMessage(error)
});
throw error;
}
}
//#endregion
export { publishVerifiedSqliteFile as a, createVerifiedSqliteSnapshot as i, sanitizeOpenClawGlobalStateSnapshot as n, sanitizeOpenClawStateLeaseRows as r, backupCreateCommand as t };