UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

2,678 lines 127 kB
import { t as containsAsciiControlCharacter } from "./string-normalization-DsCfAx8q.js";
import { n as sliceUtf16Safe, r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.js";
import { a as writeRuntimeJson, r as defaultRuntime } from "./runtime-CF2WjnNZ.js";
import { Et as array, Rn as string, Tn as object, wn as number, yt as _enum } from "./schemas-zxit8y5H.js";
import { n as getRuntimeConfig, s as readConfigFileSnapshot } from "./io.runtime-B9iJRs3w.js";
import { I as sameFileIdentity, a as canonicalPathFromExistingAncestor, c as isPathInside, o as ensureAbsoluteDirectory, w as root } from "./fs-safe-B6pvPGnf.js";
import { m as shortenHomePath } from "./utils-P__uGsPB.js";
import { n as normalizeAgentId, t as isValidAgentId } from "./agent-id-CeT3w4ap.js";
import { _ as resolveConfiguredAgentId, o as listAgentIds } from "./agent-scope-config-DcbEhP0R.js";
import { w as resolveStateDir } from "./paths-D2sRr1a_.js";
import "./session-key-BnWWjqNc.js";
import { o as redactSensitiveUrlLikeString } from "./redact-sensitive-url-BN1NZvXG.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { t as parseDurationMs } from "./parse-duration-CuuCHKpt.js";
import { t as openNodeSqliteDatabase } from "./node-sqlite-BpQX3W0e.js";
import { t as resolveSystemBin } from "./resolve-system-bin-IIzLwiri.js";
import { c as createPrivateSqliteTempDirectory, d as buildEncodedPowerShellArgs, f as buildPowerShellFailureCause, s as createPrivateSqliteDirectory, u as WINDOWS_POWERSHELL_COLD_SPAWN_TIMEOUT_MS } from "./sqlite-readonly-location-BC9PgENz.js";
import { l as assertOpenClawStateDatabaseForMaintenance, y as getOpenClawStateRuntimeSchema } from "./openclaw-state-db-cache-C7ljO0xP.js";
import { a as readSqliteUserVersion } from "./sqlite-user-version-DFJCxX41.js";
import { t as applyPrivateModeSync } from "./private-mode-B6dWGRb2.js";
import { s as resolveOpenClawStateSqlitePath } from "./openclaw-state-db-schema-version-c1ZL6JGz.js";
import { t as assertSqliteIntegrity } from "./sqlite-integrity-NpEtFIdK.js";
import "./openclaw-state-db-BRTnL-D8.js";
import "./config-Cs0XXL3x.js";
import { t as formatDocsLink } from "./links-ClIwBcy4.js";
import { r as theme } from "./theme-vjDs9tao.js";
import { a as isImplicitLocalGatewayTargetFromCli, n as callGatewayFromCli, t as addGatewayClientOptions } from "./gateway-rpc-C9fDQ0vT.js";
import { n as runExec } from "./exec-BIE-3oLG.js";
import { c as sha256File, d as syncDirectoryIfSupported, i as pinDirectory, l as syncDirectory, n as getPublishFileExclusiveFailureDetails, o as publishFileNoClobber, s as requireDirectorySync, t as ensureDurableDirectory } from "./directory-durability-CINgXRM4.js";
import { Y as OPENCLAW_AGENT_SCHEMA_SQL, t as assertOpenClawAgentDatabaseForMaintenance } from "./openclaw-agent-db-maintenance-wTIy-jt-.js";
import "./openclaw-agent-db-CWtDoRbC.js";
import { t as loadSqliteVecExtension } from "./sqlite-vec-C8wbuI1F.js";
import { n as formatCommandOutput, r as formatCommandResult } from "./command-error-D_Bz4MBL.js";
import { a as requireGitCommandBuffer, i as requireGitCommand, r as executeGitCommand } from "./git-exec-0RazRvYf.js";
import "./engine-storage-Dk7qCapl.js";
import { n as isPathWithin } from "./cleanup-utils-BDhDB9DZ.js";
import { n as runCommandWithRuntime } from "./cli-utils-D1DAWB8d.js";
import { t as formatHelpExamples } from "./help-format-CAcwboTs.js";
import { n as parseStrictPositiveIntOption } from "./helpers-B-LqXQ3Z.js";
import { r as listCronJobsFromGateway } from "./list-jobs-C_GC9a0W.js";
import { i as resolveStartupConfigSnapshot } from "./automatic-startup-config-repair-BVYl6aS8.js";
import { i as BACKUP_RUN_ERROR_MAX_LENGTH } from "./backup-run-records-BNUh01Bt.js";
import { a as publishVerifiedSqliteFile, i as createVerifiedSqliteSnapshot, n as sanitizeOpenClawGlobalStateSnapshot, r as sanitizeOpenClawStateLeaseRows, t as backupCreateCommand } from "./backup-DOTnEJSa.js";
import { a as BACKUP_MAX_DECOMPRESSION_RATIO, d as resolveBackupAgentRoot, f as resolveBackupAgentRoots, l as canonicalizePathForContainment, m as resolveRequiredBackupPath, u as recordBackupOutcomeBestEffort } from "./backup-archive-path-policy-BwxoeKmU.js";
import { n as prepareBackupArchive, t as backupVerifyCommand } from "./backup-verify-BcCCwhcS.js";
import fs from "node:fs";
import { isDeepStrictEqual } from "node:util";
import path from "node:path";
import fs$1 from "node:fs/promises";
import os from "node:os";
import { createHash, randomUUID } from "node:crypto";
import * as tar from "tar";
//#region src/state/secret-state-tables.ts
/** Redaction policy surface: Git snapshots may omit these credential-bearing tables. */
const STATE_SECRET_TABLE_NAMES = [
	"audit_identity_keys",
	"apns_registrations",
	"channel_ingress_events",
	"channel_pairing_requests",
	"clawhub_promotion_claims",
	"config_revision_keys",
	"device_auth_tokens",
	"device_bootstrap_tokens",
	"device_identities",
	"device_pairing_join_codes",
	"device_pairing_paired",
	"gateway_origin_device_tokens",
	"mcp_oauth_pending_authorizations",
	"mcp_oauth_stores",
	"native_hook_relay_bridges",
	"secret_store_entries",
	"web_push_subscriptions",
	"worker_environment_credentials"
];
/** Secret-redacted Git backups must never carry machine-state values under these prefixes. */
const STATE_SECRET_CONFIG_STATE_KEY_PREFIXES = [
	"authProfiles.",
	"nodeHost.",
	"webPush.vapidKeys"
];
/** Redaction policy surface for credential-bearing per-agent database tables. */
const AGENT_SECRET_TABLE_NAMES = [
	"auth_profile_state",
	"auth_profile_store",
	"session_suggestions"
];
//#endregion
//#region src/snapshot/snapshot-provider.ts
const SNAPSHOT_MANIFEST_FILENAME = "manifest.json";
const SNAPSHOT_SQLITE_FILENAME = "database.sqlite";
//#endregion
//#region src/snapshot/manifest.ts
const MAX_MANIFEST_BYTES = 1048576;
const MAX_SQLITE_USER_VERSION = 2147483647;
const MIN_SQLITE_USER_VERSION = -2147483648;
const SNAPSHOT_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,254}$/;
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
async function hashSnapshotArtifact(snapshotDir) {
	const opened = await (await root(snapshotDir)).open(SNAPSHOT_SQLITE_FILENAME, {
		hardlinks: "reject",
		symlinks: "reject"
	});
	try {
		return {
			...await hashFileHandle(opened.handle),
			stat: opened.stat
		};
	} finally {
		await opened.handle.close();
	}
}
async function copySnapshotArtifact(snapshotDir, targetPath) {
	const source = await (await root(snapshotDir)).open(SNAPSHOT_SQLITE_FILENAME, {
		hardlinks: "reject",
		symlinks: "reject"
	});
	let target;
	let targetIdentity;
	try {
		target = await fs$1.open(targetPath, "wx+", 384);
		targetIdentity = await target.stat();
		const digest = await hashFileHandle(source.handle, target);
		await target.sync();
		const finalIdentity = await target.stat();
		const currentIdentity = await fs$1.lstat(targetPath);
		if (!sameFileIdentity(targetIdentity, finalIdentity) || !sameFileIdentity(targetIdentity, currentIdentity)) throw new Error(`Snapshot restore staging file changed during copy: ${targetPath}`);
		return {
			...digest,
			stat: finalIdentity
		};
	} catch (error) {
		await target?.close().catch(() => void 0);
		target = void 0;
		if (targetIdentity) {
			const currentIdentity = await fs$1.lstat(targetPath).catch(() => void 0);
			if (currentIdentity && sameFileIdentity(targetIdentity, currentIdentity)) await fs$1.unlink(targetPath).catch(() => void 0);
		}
		throw error;
	} finally {
		await target?.close().catch(() => void 0);
		await source.handle.close().catch(() => void 0);
	}
}
async function hashFileHandle(source, target) {
	const initialStat = await source.stat({ bigint: true });
	let sizeBytes = 0;
	if (target) {
		const buffer = Buffer.allocUnsafe(1048576);
		while (true) {
			const { bytesRead } = await source.read(buffer, 0, buffer.length, sizeBytes);
			if (bytesRead === 0) break;
			let bytesWritten = 0;
			while (bytesWritten < bytesRead) {
				const result = await target.write(buffer, bytesWritten, bytesRead - bytesWritten, sizeBytes + bytesWritten);
				if (result.bytesWritten === 0) throw new Error("Snapshot restore staging copy made no progress.");
				bytesWritten += result.bytesWritten;
			}
			sizeBytes += bytesRead;
		}
	}
	const hashed = await sha256File(target ?? source);
	if (!sameMutationFingerprint(initialStat, await source.stat({ bigint: true })) || target && sizeBytes !== hashed.bytes) throw new Error("Snapshot artifact changed while being read.");
	return {
		sha256: hashed.digest,
		sizeBytes: hashed.bytes
	};
}
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 writeSnapshotManifest(snapshotDir, manifest) {
	const manifestPath = path.join(snapshotDir, SNAPSHOT_MANIFEST_FILENAME);
	const handle = await fs$1.open(manifestPath, "wx+", 384);
	try {
		await handle.writeFile(`${JSON.stringify(manifest, null, 2)}\n`, "utf8");
		await handle.sync();
	} finally {
		await handle.close();
	}
}
async function readSnapshotManifest(snapshotDir, expectedSnapshotId = path.basename(snapshotDir)) {
	const snapshotRoot = await root(snapshotDir);
	const manifestPath = path.join(snapshotDir, SNAPSHOT_MANIFEST_FILENAME);
	const result = await snapshotRoot.read(SNAPSHOT_MANIFEST_FILENAME, {
		hardlinks: "reject",
		maxBytes: MAX_MANIFEST_BYTES,
		symlinks: "reject"
	});
	let parsed;
	try {
		parsed = JSON.parse(result.buffer.toString("utf8"));
	} catch (error) {
		throw new Error(`Snapshot manifest is not valid JSON: ${manifestPath}`, { cause: error });
	}
	return parseSnapshotManifest(parsed, manifestPath, expectedSnapshotId);
}
function parseSnapshotManifest(value, manifestPath, expectedSnapshotId) {
	const record = requireRecord(value, "manifest", manifestPath);
	requireExactKeys(record, [
		"schemaVersion",
		"snapshotId",
		"createdAt",
		"database",
		"artifact"
	]);
	if (record.schemaVersion !== 1) throw new Error(`Unsupported snapshot manifest schemaVersion ${String(record.schemaVersion)}: ${manifestPath}`);
	const snapshotId = requireSnapshotId(record.snapshotId, manifestPath);
	if (snapshotId !== expectedSnapshotId) throw new Error(`Snapshot manifest id ${snapshotId} does not match directory ${expectedSnapshotId}: ${manifestPath}`);
	const createdAt = requireCanonicalTimestamp(record.createdAt, manifestPath);
	const database = parseSnapshotDatabase(record.database, manifestPath);
	const artifactRecord = requireRecord(record.artifact, "artifact", manifestPath);
	requireExactKeys(artifactRecord, [
		"path",
		"sha256",
		"sizeBytes"
	]);
	if (artifactRecord.path !== "database.sqlite") throw new Error(`Snapshot manifest artifact.path must be ${SNAPSHOT_SQLITE_FILENAME}: ${manifestPath}`);
	if (typeof artifactRecord.sha256 !== "string" || !SHA256_PATTERN.test(artifactRecord.sha256)) throw new Error(`Snapshot manifest artifact.sha256 is invalid: ${manifestPath}`);
	if (!Number.isSafeInteger(artifactRecord.sizeBytes) || Number(artifactRecord.sizeBytes) <= 0) throw new Error(`Snapshot manifest artifact.sizeBytes is invalid: ${manifestPath}`);
	return {
		schemaVersion: 1,
		snapshotId,
		createdAt,
		database,
		artifact: {
			path: SNAPSHOT_SQLITE_FILENAME,
			sha256: artifactRecord.sha256,
			sizeBytes: Number(artifactRecord.sizeBytes)
		}
	};
}
function parseSnapshotDatabase(value, manifestPath) {
	const database = requireRecord(value, "database", manifestPath);
	const role = database.role;
	const basename = requireSafeText(database.basename, "database.basename", manifestPath, 255);
	if (path.basename(basename) !== basename || basename === "." || basename === "..") throw new Error(`Snapshot manifest database.basename is invalid: ${manifestPath}`);
	const userVersion = requireSqliteUserVersion(database.userVersion, manifestPath);
	if (role === "global") {
		requireExactKeys(database, [
			"role",
			"basename",
			"userVersion"
		]);
		return {
			role,
			basename,
			userVersion
		};
	}
	if (role === "agent") {
		requireExactKeys(database, [
			"role",
			"agentId",
			"basename",
			"userVersion"
		]);
		const agentId = requireSafeText(database.agentId, "database.agentId", manifestPath, 64);
		if (!isValidAgentId(agentId) || normalizeAgentId(agentId) !== agentId) throw new Error(`Snapshot manifest database.agentId is invalid: ${manifestPath}`);
		return {
			role,
			agentId,
			basename,
			userVersion
		};
	}
	if (role === "generic") {
		requireExactKeys(database, [
			"role",
			"id",
			"basename",
			"userVersion"
		]);
		return {
			role,
			id: requireSafeText(database.id, "database.id", manifestPath, 256),
			basename,
			userVersion
		};
	}
	throw new Error(`Snapshot manifest database.role is invalid: ${manifestPath}`);
}
function requireRecord(value, field, manifestPath) {
	if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Snapshot manifest ${field} must be an object: ${manifestPath}`);
	return value;
}
function requireExactKeys(record, expectedKeys) {
	const actual = Object.keys(record).toSorted();
	const expected = [...expectedKeys].toSorted();
	if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) throw new Error(`Snapshot manifest fields must be exactly ${expectedKeys.join(", ")}; got ${actual.join(", ")}`);
}
function requireSnapshotId(value, manifestPath) {
	if (typeof value !== "string" || !SNAPSHOT_ID_PATTERN.test(value)) throw new Error(`Snapshot manifest snapshotId is invalid: ${manifestPath}`);
	return value;
}
function requireCanonicalTimestamp(value, manifestPath) {
	if (typeof value !== "string") throw new Error(`Snapshot manifest createdAt is invalid: ${manifestPath}`);
	const parsed = new Date(value);
	if (!Number.isFinite(parsed.getTime()) || parsed.toISOString() !== value) throw new Error(`Snapshot manifest createdAt is not canonical ISO 8601: ${manifestPath}`);
	return value;
}
function requireSafeText(value, field, manifestPath, maxLength) {
	if (typeof value !== "string" || value.length === 0 || value.length > maxLength || value.trim() !== value || containsAsciiControlCharacter(value)) throw new Error(`Snapshot manifest ${field} is invalid: ${manifestPath}`);
	return value;
}
function requireSqliteUserVersion(value, manifestPath) {
	if (!Number.isSafeInteger(value) || Number(value) < MIN_SQLITE_USER_VERSION || Number(value) > MAX_SQLITE_USER_VERSION) throw new Error(`Snapshot manifest database.userVersion is invalid: ${manifestPath}`);
	return Number(value);
}
//#endregion
//#region src/snapshot/openclaw-snapshot-copy.ts
function normalizeSnapshotIdentity(identity) {
	if (identity.role === "global") return identity;
	if (identity.role === "agent") {
		const agentId = normalizeAgentId(identity.agentId);
		if (!isValidAgentId(identity.agentId) || agentId !== identity.agentId) throw new Error(`SQLite snapshot agent id must be canonical: ${identity.agentId}`);
		return {
			role: "agent",
			agentId
		};
	}
	const id = identity.id.trim();
	if (!id || id !== identity.id || id.length > 256 || containsAsciiControlCharacter(id)) throw new Error("SQLite snapshot generic database id is invalid.");
	return {
		role: "generic",
		id
	};
}
function buildSnapshotValidator(identity) {
	if (identity.role === "global") return (database, pathname) => assertOpenClawStateDatabaseForMaintenance(database, { pathname });
	if (identity.role === "agent") return (database, pathname) => assertOpenClawAgentDatabaseForMaintenance(database, {
		agentId: identity.agentId,
		pathname
	});
	return () => void 0;
}
/** Produce the canonical sanitized, compact, verified copy used by every snapshot provider. */
async function createOpenClawSnapshotCopy(params) {
	const identity = normalizeSnapshotIdentity(params.database.identity);
	return {
		identity,
		...await createVerifiedSqliteSnapshot({
			sourcePath: params.database.path,
			targetPath: params.targetPath,
			requireNonEmptySource: identity.role !== "generic",
			transform: identity.role === "global" ? sanitizeOpenClawGlobalStateSnapshot : identity.role === "agent" ? sanitizeOpenClawStateLeaseRows : void 0,
			validate: buildSnapshotValidator(identity)
		})
	};
}
//#endregion
//#region src/snapshot/git-backup-codec.ts
const GIT_BACKUP_MANIFEST = "manifest.json";
const GIT_BACKUP_SCHEMA = "schema.sql";
const GIT_BACKUP_TABLES = "tables";
const SQLITE_SIDECAR_SUFFIXES$1 = [
	"-wal",
	"-shm",
	"-journal"
];
const SAFE_TABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
const GIT_BACKUP_PROJECTION_TABLES = ["backup_runs", "session_transcript_index_state"];
function quoteIdentifier(value) {
	return `"${value.replaceAll("\"", "\"\"")}"`;
}
function requireSafeTableName(value) {
	if (!SAFE_TABLE_NAME.test(value)) throw new Error(`Git backup table name is not filesystem-safe: ${value}`);
	return value;
}
function sha256(value) {
	return createHash("sha256").update(value).digest("hex");
}
function normalizeIdentity(identity) {
	if (identity.role === "global") return identity;
	const agentId = normalizeAgentId(identity.agentId);
	if (agentId !== identity.agentId) throw new Error(`Git backup agent id must be canonical: ${identity.agentId}`);
	return {
		role: "agent",
		agentId
	};
}
function gitBackupScopePath(identity) {
	const normalized = normalizeIdentity(identity);
	return normalized.role === "global" ? "global" : path.join("agents", normalized.agentId);
}
function readSchemaEntries(database) {
	return database.prepare(`SELECT type, name, tbl_name AS tableName, sql
         FROM sqlite_master
        WHERE type IN ('table', 'index', 'trigger')
          AND name NOT LIKE 'sqlite_%'
          AND sql IS NOT NULL
        ORDER BY CASE type WHEN 'table' THEN 0 WHEN 'index' THEN 1 ELSE 2 END, name`).all().map((row) => row);
}
function virtualTableNames(entries) {
	return entries.filter((entry) => /^\s*CREATE\s+VIRTUAL\s+TABLE\b/iu.test(entry.sql)).map((entry) => entry.name);
}
function isVirtualShadow(name, virtualTables) {
	return virtualTables.some((virtualTable) => name === virtualTable || name.startsWith(`${virtualTable}_`));
}
function readTableColumns(database, table) {
	return database.prepare(`PRAGMA table_info(${quoteIdentifier(table)})`).all().map((row) => {
		const value = row;
		if (typeof value.name !== "string" || typeof value.pk !== "number") throw new Error(`Unable to read columns for Git backup table ${table}.`);
		return {
			name: value.name,
			pk: value.pk
		};
	});
}
function encodeSqliteValue(value) {
	if (value === null || typeof value === "string") return value;
	if (typeof value === "number") {
		if (!Number.isFinite(value)) throw new Error("Git backup cannot encode a non-finite SQLite REAL value.");
		return value;
	}
	if (typeof value === "bigint") return value >= Number.MIN_SAFE_INTEGER && value <= Number.MAX_SAFE_INTEGER ? Number(value) : { $int: value.toString() };
	if (value instanceof Uint8Array) return { $hex: Buffer.from(value).toString("hex") };
	throw new Error(`Git backup cannot encode SQLite value type ${typeof value}.`);
}
function serializeTable(database, table, rowFilter) {
	const columns = readTableColumns(database, table);
	if (columns.length === 0) throw new Error(`Git backup table has no readable columns: ${table}`);
	const primaryKey = columns.filter((column) => column.pk > 0).toSorted((left, right) => left.pk - right.pk).map((column) => `source.${quoteIdentifier(column.name)}`);
	const orderBy = primaryKey.length > 0 ? primaryKey.join(", ") : "source.rowid";
	const projection = columns.map(({ name }) => {
		const column = quoteIdentifier(name);
		return `CASE WHEN typeof(${column}) = 'text' THEN json_quote(${column}) ELSE ${column} END AS ${column}`;
	});
	const statement = database.prepare(`SELECT ${projection.join(", ")}
       FROM ${quoteIdentifier(table)} AS source ORDER BY ${orderBy}`);
	statement.setReadBigInts(true);
	const lines = [];
	for (const rawRow of statement.iterate()) {
		const source = {};
		for (const [name, value] of Object.entries(rawRow)) source[name] = typeof value === "string" ? JSON.parse(value) : value;
		if (rowFilter && !rowFilter(source)) continue;
		const encoded = {};
		for (const column of columns) encoded[column.name] = encodeSqliteValue(source[column.name]);
		lines.push(JSON.stringify(encoded));
	}
	return {
		content: lines.length > 0 ? `${lines.join("\n")}\n` : "",
		rows: lines.length
	};
}
function schemaText(entries, userVersion) {
	return `${entries.map((entry) => entry.sql.trimEnd().endsWith(";") ? entry.sql : `${entry.sql};`).join("\n\n")}\n-- PRAGMA user_version = ${userVersion}\n`;
}
function redactedSecretTables(identity, excludeSecrets) {
	if (!excludeSecrets) return /* @__PURE__ */ new Set();
	return new Set(identity.role === "global" ? STATE_SECRET_TABLE_NAMES : AGENT_SECRET_TABLE_NAMES);
}
/** Dump one verified SQLite copy into the deterministic Git repository layout. */
async function dumpGitBackupDatabase(params) {
	const identity = normalizeIdentity(params.identity);
	const database = openNodeSqliteDatabase(params.snapshotPath, { readOnly: true });
	try {
		const entries = readSchemaEntries(database);
		const virtualTables = virtualTableNames(entries);
		const redacted = redactedSecretTables(identity, params.excludeSecrets === true);
		const existingTables = new Set(entries.filter((entry) => entry.type === "table").map((entry) => entry.name));
		const excludedTables = [...redacted].filter((table) => existingTables.has(table)).toSorted();
		const excludedConfigStateKeyPrefixes = identity.role === "global" && params.excludeSecrets === true && existingTables.has("config_machine_state") ? [...STATE_SECRET_CONFIG_STATE_KEY_PREFIXES] : [];
		const excluded = /* @__PURE__ */ new Set([...excludedTables, ...GIT_BACKUP_PROJECTION_TABLES]);
		const includedSchema = entries.filter((entry) => !excluded.has(entry.name) && !excluded.has(entry.tableName));
		const dataTables = entries.filter((entry) => entry.type === "table" && !isVirtualShadow(entry.name, virtualTables) && !excluded.has(entry.name)).map((entry) => requireSafeTableName(entry.name)).toSorted();
		const userVersionRow = database.prepare("PRAGMA user_version").get();
		if (typeof userVersionRow.user_version !== "number") throw new Error("Unable to read SQLite user_version for Git backup.");
		await fs$1.rm(params.outputPath, {
			recursive: true,
			force: true
		});
		const tablesPath = path.join(params.outputPath, GIT_BACKUP_TABLES);
		await fs$1.mkdir(tablesPath, {
			recursive: true,
			mode: 448
		});
		const tables = {};
		for (const table of dataTables) {
			const serialized = serializeTable(database, table, table === "config_machine_state" && excludedConfigStateKeyPrefixes.length > 0 ? (row) => {
				const stateKey = row.state_key;
				return typeof stateKey === "string" && !excludedConfigStateKeyPrefixes.some((prefix) => stateKey.startsWith(prefix));
			} : void 0);
			await fs$1.writeFile(path.join(tablesPath, `${table}.jsonl`), serialized.content, {
				encoding: "utf8",
				mode: 384
			});
			tables[table] = {
				rows: serialized.rows,
				sha256: sha256(serialized.content)
			};
		}
		const manifest = {
			schemaVersion: 1,
			identity,
			userVersion: userVersionRow.user_version,
			excludedTables,
			excludedConfigStateKeyPrefixes,
			tables
		};
		await fs$1.writeFile(path.join(params.outputPath, GIT_BACKUP_SCHEMA), schemaText(includedSchema, manifest.userVersion), {
			encoding: "utf8",
			mode: 384
		});
		await fs$1.writeFile(path.join(params.outputPath, GIT_BACKUP_MANIFEST), `${JSON.stringify(manifest, null, 2)}\n`, {
			encoding: "utf8",
			mode: 384
		});
		return manifest;
	} finally {
		database.close();
	}
}
function parseGitBackupManifest(value, source) {
	let parsed;
	try {
		parsed = JSON.parse(value);
	} catch (error) {
		throw new Error(`Git backup manifest is invalid JSON: ${source}`, { cause: error });
	}
	if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`Git backup manifest is invalid: ${source}`);
	const manifest = parsed;
	if (manifest.schemaVersion !== 1 || !manifest.identity || manifest.identity.role !== "global" && manifest.identity.role !== "agent" || !Number.isSafeInteger(manifest.userVersion) || !Array.isArray(manifest.excludedTables) || manifest.excludedConfigStateKeyPrefixes !== void 0 && (!Array.isArray(manifest.excludedConfigStateKeyPrefixes) || manifest.excludedConfigStateKeyPrefixes.some((prefix) => typeof prefix !== "string")) || !manifest.tables || typeof manifest.tables !== "object") throw new Error(`Git backup manifest has unsupported fields: ${source}`);
	const validated = {
		...manifest,
		excludedConfigStateKeyPrefixes: manifest.excludedConfigStateKeyPrefixes ?? []
	};
	normalizeIdentity(validated.identity);
	for (const [table, entry] of Object.entries(validated.tables)) {
		requireSafeTableName(table);
		if (!Number.isSafeInteger(entry.rows) || entry.rows < 0 || !/^[a-f0-9]{64}$/u.test(entry.sha256)) throw new Error(`Git backup manifest has an invalid table entry: ${table}`);
	}
	return validated;
}
function splitSchemaStatements(schema) {
	const statements = [];
	let start = 0;
	let quote;
	let lineComment = false;
	let blockComment = false;
	for (let index = 0; index < schema.length; index += 1) {
		const character = schema[index];
		const next = schema[index + 1];
		if (lineComment) {
			if (character === "\n") lineComment = false;
			continue;
		}
		if (blockComment) {
			if (character === "*" && next === "/") {
				blockComment = false;
				index += 1;
			}
			continue;
		}
		if (quote) {
			if (quote === "]" && character === "]" || quote !== "]" && character === quote) {
				if (quote !== "]" && next === quote) index += 1;
				else quote = void 0;
			}
			continue;
		}
		if (character === "-" && next === "-") {
			lineComment = true;
			index += 1;
			continue;
		}
		if (character === "/" && next === "*") {
			blockComment = true;
			index += 1;
			continue;
		}
		if (character === "'" || character === "\"" || character === "`") {
			quote = character;
			continue;
		}
		if (character === "[") {
			quote = "]";
			continue;
		}
		if (character !== ";") continue;
		const candidate = schema.slice(start, index + 1).trim();
		if (/^CREATE\s+TRIGGER\b/iu.test(candidate) && !/\bEND\s*;$/iu.test(candidate)) continue;
		if (candidate && !candidate.startsWith("-- PRAGMA user_version")) statements.push(candidate);
		start = index + 1;
	}
	return statements;
}
function unquoteSqlIdentifier(value) {
	if (value.startsWith("'")) return value.slice(1, -1).replaceAll("''", "'");
	if (value.startsWith("\"")) return value.slice(1, -1).replaceAll("\"\"", "\"");
	if (value.startsWith("`")) return value.slice(1, -1).replaceAll("``", "`");
	if (value.startsWith("[")) return value.slice(1, -1);
	return value;
}
function schemaObjectName(statement, kind) {
	const match = new RegExp(`^${kind === "virtual" ? "CREATE\\s+VIRTUAL\\s+TABLE" : "CREATE\\s+TABLE"}\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?('(?:[^']|'')*'|"(?:[^"]|"")*"|\\[[^\\]]+\\]|\`(?:[^\`]|\`\`)*\`|[^\\s(]+)`, "iu").exec(statement);
	return match?.[1] ? unquoteSqlIdentifier(match[1]) : void 0;
}
function decodeSqliteValue(value) {
	if (value === null || typeof value === "string" || typeof value === "number") return value;
	if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Git backup row contains an invalid encoded value.");
	const record = value;
	if (Object.keys(record).length === 1 && typeof record.$int === "string") return BigInt(record.$int);
	if (Object.keys(record).length === 1 && typeof record.$hex === "string" && /^(?:[a-f0-9]{2})*$/u.test(record.$hex)) return Buffer.from(record.$hex, "hex");
	throw new Error("Git backup row contains an invalid encoded object.");
}
async function assertFreshRestoreTarget(targetPath) {
	for (const candidate of [targetPath, ...SQLITE_SIDECAR_SUFFIXES$1.map((suffix) => `${targetPath}${suffix}`)]) {
		try {
			await fs$1.lstat(candidate);
		} catch (error) {
			if (error.code === "ENOENT") continue;
			throw error;
		}
		throw new Error(`Fresh SQLite restore path already exists: ${candidate}`);
	}
}
function assertNoSqliteSidecarsSync$1(targetPath) {
	for (const suffix of SQLITE_SIDECAR_SUFFIXES$1) {
		const sidecarPath = `${targetPath}${suffix}`;
		try {
			fs.lstatSync(sidecarPath);
		} catch (error) {
			if (error.code === "ENOENT") continue;
			throw error;
		}
		throw new Error(`Fresh SQLite restore path already exists: ${sidecarPath}`);
	}
}
function convergeRestoredSchema(database, identity) {
	database.exec(identity.role === "global" ? getOpenClawStateRuntimeSchema({ includeVersionLazyAdditiveTables: false }) : OPENCLAW_AGENT_SCHEMA_SQL);
}
function validateRestoredOwner(database, databasePath, identity) {
	assertSqliteIntegrity(database, databasePath);
	if (database.prepare("PRAGMA foreign_key_check").all().length > 0) throw new Error(`SQLite foreign_key_check failed for restored Git backup: ${databasePath}`);
	buildSnapshotValidator(identity)(database, databasePath);
}
function loadTable(database, table, content) {
	const columns = readTableColumns(database, table);
	const statement = database.prepare(`INSERT INTO ${quoteIdentifier(table)} (${columns.map((column) => quoteIdentifier(column.name)).join(", ")})
     VALUES (${columns.map(() => "?").join(", ")})`);
	let rows = 0;
	for (const line of content.split("\n")) {
		if (!line) continue;
		const parsed = JSON.parse(line);
		statement.run(...columns.map((column) => decodeSqliteValue(parsed[column.name])));
		rows += 1;
	}
	return rows;
}
/** Restore one materialized Git snapshot scope into a fresh SQLite file. */
async function restoreGitBackupDirectory(params) {
	const targetPath = path.resolve(params.targetPath);
	await assertFreshRestoreTarget(targetPath);
	const manifest = parseGitBackupManifest(await fs$1.readFile(path.join(params.sourcePath, GIT_BACKUP_MANIFEST), "utf8"), params.sourcePath);
	const restoreIdentity = normalizeIdentity(params.expectedIdentity ?? manifest.identity);
	if (params.expectedIdentity && JSON.stringify(normalizeIdentity(manifest.identity)) !== JSON.stringify(restoreIdentity)) throw new Error("Git backup manifest database identity does not match the requested scope.");
	const statements = splitSchemaStatements(await fs$1.readFile(path.join(params.sourcePath, GIT_BACKUP_SCHEMA), "utf8"));
	const virtual = statements.filter((statement) => /^CREATE\s+VIRTUAL\s+TABLE\b/iu.test(statement));
	const triggers = statements.filter((statement) => /^CREATE\s+TRIGGER\b/iu.test(statement));
	const virtualNames = virtual.map((statement) => schemaObjectName(statement, "virtual")).filter((value) => Boolean(value));
	const plainTables = statements.filter((statement) => {
		if (!/^CREATE\s+TABLE\b/iu.test(statement)) return false;
		const name = schemaObjectName(statement, "table");
		return !name || !isVirtualShadow(name, virtualNames);
	});
	const indexes = statements.filter((statement) => /^CREATE\s+(?:UNIQUE\s+)?INDEX\b/iu.test(statement));
	const targetDirectory = path.dirname(targetPath);
	await fs$1.mkdir(targetDirectory, {
		recursive: true,
		mode: 448
	});
	const stagingDirectory = await createPrivateSqliteTempDirectory(targetDirectory, ".git-backup-restore-");
	applyPrivateModeSync(stagingDirectory, 448);
	const stagedPath = path.join(stagingDirectory, SNAPSHOT_SQLITE_FILENAME);
	await (await fs$1.open(stagedPath, "wx", 384)).close();
	const database = openNodeSqliteDatabase(stagedPath);
	try {
		database.exec("PRAGMA foreign_keys = OFF; PRAGMA journal_mode = DELETE;");
		for (const statement of [...plainTables, ...indexes]) database.exec(statement);
		database.exec("BEGIN IMMEDIATE;");
		try {
			for (const [table, expected] of Object.entries(manifest.tables)) {
				requireSafeTableName(table);
				const content = await fs$1.readFile(path.join(params.sourcePath, GIT_BACKUP_TABLES, `${table}.jsonl`), "utf8");
				if (sha256(content) !== expected.sha256) throw new Error(`Git backup table hash mismatch: ${table}`);
				if (loadTable(database, table, content) !== expected.rows) throw new Error(`Git backup table row count mismatch: ${table}`);
			}
			database.exec("COMMIT;");
		} catch (error) {
			database.exec("ROLLBACK;");
			throw error;
		}
		for (const statement of virtual) {
			if (/\bUSING\s+vec0\b/iu.test(statement)) continue;
			database.exec(statement);
		}
		for (const statement of triggers) database.exec(statement);
		for (const statement of virtual) {
			const name = schemaObjectName(statement, "virtual");
			if (name && /\bUSING\s+fts5\b/iu.test(statement) && /\bcontent\s*=/iu.test(statement)) database.prepare(`INSERT INTO ${quoteIdentifier(name)} (${quoteIdentifier(name)}) VALUES ('rebuild')`).run();
		}
		database.exec(`PRAGMA user_version = ${manifest.userVersion};`);
		convergeRestoredSchema(database, restoreIdentity);
		validateRestoredOwner(database, stagedPath, restoreIdentity);
		const tables = Object.entries(manifest.tables).map(([table, expected]) => {
			const actual = serializeTable(database, table);
			const actualSha256 = sha256(actual.content);
			return {
				table,
				rows: actual.rows,
				sha256: actualSha256,
				ok: actual.rows === expected.rows && actualSha256 === expected.sha256
			};
		});
		if (tables.some((table) => !table.ok)) throw new Error(`Restored Git backup does not match its table manifest: ${stagedPath}`);
		database.close();
		applyPrivateModeSync(stagedPath, 384);
		const artifact = await hashSnapshotArtifact(stagingDirectory);
		await publishVerifiedSqliteFile({
			sourceIdentity: artifact.stat,
			sourcePath: stagedPath,
			targetPath,
			expectedContent: artifact,
			requireAtomicPublication: true,
			beforePublish: async () => await assertFreshRestoreTarget(targetPath),
			validatePublished: async (publishedPath) => {
				const published = openNodeSqliteDatabase(publishedPath, { readOnly: true });
				try {
					validateRestoredOwner(published, publishedPath, restoreIdentity);
				} finally {
					published.close();
				}
			},
			afterPublish: (guard) => {
				guard.assertTargetMatchesExpectedContent(() => assertNoSqliteSidecarsSync$1(targetPath));
			}
		});
		return {
			manifest,
			targetPath,
			tables,
			excludedTables: manifest.excludedTables,
			excludedConfigStateKeyPrefixes: manifest.excludedConfigStateKeyPrefixes ?? []
		};
	} catch (error) {
		if (database.isOpen) database.close();
		throw error;
	} finally {
		await fs$1.rm(stagingDirectory, {
			recursive: true,
			force: true
		}).catch(() => void 0);
	}
}
//#endregion
//#region src/snapshot/local-repository.ts
const SNAPSHOT_DIRECTORY_MODE = 448;
const SNAPSHOT_FILE_MODE = 384;
const SNAPSHOT_PENDING_FILENAME = ".pending";
const SQLITE_SIDECAR_SUFFIXES = [
	"-wal",
	"-shm",
	"-journal"
];
const SNAPSHOT_ARTIFACT_ENTRIES = /* @__PURE__ */ new Set([
	SNAPSHOT_MANIFEST_FILENAME,
	SNAPSHOT_PENDING_FILENAME,
	SNAPSHOT_SQLITE_FILENAME
]);
const RESTORE_STAGING_ENTRIES = /* @__PURE__ */ new Set([SNAPSHOT_SQLITE_FILENAME]);
const VALIDATION_STAGING_ENTRIES = /* @__PURE__ */ new Set([SNAPSHOT_SQLITE_FILENAME, ...SQLITE_SIDECAR_SUFFIXES.map((suffix) => `${SNAPSHOT_SQLITE_FILENAME}${suffix}`)]);
const MACOS_REPLACEMENT_ACL_PERMISSIONS = /* @__PURE__ */ new Set([
	"add_file",
	"add_subdirectory",
	"chown",
	"delete",
	"delete_child",
	"writesecurity"
]);
const WINDOWS_STAGING_ACCESS_RIGHTS = /* @__PURE__ */ new Set([
	"F",
	"M",
	"RX",
	"R",
	"W",
	"D",
	"DE",
	"RC",
	"WDAC",
	"WO",
	"AS",
	"MA",
	"GR",
	"GW",
	"GE",
	"GA",
	"RD",
	"WD",
	"AD",
	"REA",
	"WEA",
	"X",
	"DC",
	"RA",
	"WA",
	"UNKNOWN"
]);
const WINDOWS_STAGING_REPLACEMENT_RIGHTS = /* @__PURE__ */ new Set([
	"F",
	"M",
	"D",
	"DE",
	"WDAC",
	"WO",
	"MA",
	"GA",
	"DC",
	"UNKNOWN"
]);
const WINDOWS_TRUSTED_OWNER_SIDS = /* @__PURE__ */ new Set([
	"S-1-5-18",
	"S-1-5-32-544",
	"S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464"
]);
const WINDOWS_TRUSTED_ACCESS_SIDS = /* @__PURE__ */ new Set([...WINDOWS_TRUSTED_OWNER_SIDS, "S-1-3-0"]);
const WINDOWS_ACL_METADATA_MAX_BUFFER = 16777216;
const WINDOWS_SID_SCHEMA = string().regex(/^S-\d+-\d+(?:-\d+)+$/iu).transform((value) => value.toUpperCase());
const WINDOWS_PRINCIPAL_SCHEMA = string().min(1).transform((value) => value.toUpperCase());
const WINDOWS_ACCESS_ENTRY_SCHEMA = object({
	principal: WINDOWS_PRINCIPAL_SCHEMA,
	accessType: _enum(["Allow", "Deny"]),
	rightsMask: number().int().nonnegative().max(4294967295),
	inheritanceFlags: string(),
	propagationFlags: string()
}).strict();
const WINDOWS_PATH_SECURITY_SCHEMA = object({
	currentUserSid: WINDOWS_SID_SCHEMA,
	paths: array(object({
		path: string().min(1),
		ownerSid: WINDOWS_SID_SCHEMA,
		entries: array(WINDOWS_ACCESS_ENTRY_SCHEMA).min(1)
	}).strict()).min(1)
}).strict();
const WINDOWS_FILE_RIGHTS = [
	[1, "RD"],
	[2, "WD"],
	[4, "AD"],
	[8, "REA"],
	[16, "WEA"],
	[32, "X"],
	[64, "DC"],
	[128, "RA"],
	[256, "WA"],
	[65536, "D"],
	[131072, "RC"],
	[262144, "WDAC"],
	[524288, "WO"],
	[1048576, "S"],
	[33554432, "MA"],
	[268435456, "GA"],
	[536870912, "GE"],
	[1073741824, "GW"],
	[2147483648, "GR"]
];
const WINDOWS_KNOWN_FILE_RIGHTS_MASK = WINDOWS_FILE_RIGHTS.reduce((mask, [right]) => mask | right, 0);
const WINDOWS_READ_RIGHTS_MASK = -1342046039;
const WINDOWS_WRITE_RIGHTS_MASK = 1343029590;
let macosTrustedAclPrincipalsPromise;
function createLocalSqliteSnapshotProvider(options) {
	return new LocalSqliteSnapshotProvider(options);
}
var LocalSqliteSnapshotProvider = class {
	#allowedDatabaseRoles;
	#repositoryPath;
	#validationRootPath;
	#now;
	constructor(options) {
		this.#allowedDatabaseRoles = options.allowedDatabaseRoles;
		this.#repositoryPath = path.resolve(options.repositoryPath);
		this.#validationRootPath = path.resolve(options.validationRootPath ?? path.dirname(this.#repositoryPath));
		this.#now = options.now ?? (() => /* @__PURE__ */ new Date());
	}
	async create(database) {
		const repositoryReceipt = await ensurePrivateDirectory(this.#repositoryPath, "SQLite snapshot repository");
		const repositoryIdentity = repositoryReceipt.identity;
		const trustedRepositoryPath = await assertTrustedStagingRoot(repositoryIdentity, this.#repositoryPath);
		const sourcePath = path.resolve(database.path);
		const identity = normalizeSnapshotIdentity(database.identity);
		const now = this.#now();
		if (!Number.isFinite(now.getTime())) throw new Error("SQLite snapshot timestamp is invalid.");
		const snapshotId = buildSnapshotId(now);
		const snapshotRefPath = path.join(this.#repositoryPath, snapshotId);
		const snapshotDir = path.join(trustedRepositoryPath, snapshotId);
		const stagingDir = path.join(trustedRepositoryPath, `.tmp-${randomUUID()}`);
		const artifactPath = path.join(stagingDir, SNAPSHOT_SQLITE_FILENAME);
		await assertDirectoryIdentity(trustedRepositoryPath, repositoryIdentity);
		await createPrivateSqliteDirectory(stagingDir);
		let stagingIdentity;
		let publishedDirectory;
		let publishedIdentity;
		const publishedEntries = /* @__PURE__ */ new Map();
		let snapshotDirectoryCreated = false;
		try {
			await assertDirectoryIdentity(trustedRepositoryPath, repositoryIdentity);
			stagingIdentity = await fs$1.lstat(stagingDir);
			applyPrivateModeSync(stagingDir, SNAPSHOT_DIRECTORY_MODE);
			await assertPrivateStagingDirectory(stagingIdentity, stagingDir);
			await assertDirectoryIdentity(trustedRepositoryPath, repositoryIdentity);
			const result = await createOpenClawSnapshotCopy({
				database: {
					path: sourcePath,
					identity
				},
				targetPath: artifactPath
			});
			applyPrivateModeSync(artifactPath, SNAPSHOT_FILE_MODE);
			const artifact = await hashSnapshotArtifact(stagingDir);
			const manifest = {
				schemaVersion: 1,
				snapshotId,
				createdAt: now.toISOString(),
				database: buildDatabaseManifest(identity, sourcePath, result.userVersion),
				artifact: {
					path: SNAPSHOT_SQLITE_FILENAME,
					sha256: artifact.sha256,
					sizeBytes: artifact.sizeBytes
				}
			};
			await writeSnapshotManifest(stagingDir, manifest);
			applyPrivateModeSync(path.join(stagingDir, SNAPSHOT_MANIFEST_FILENAME), SNAPSHOT_FILE_MODE);
			await readSnapshotManifest(stagingDir, snapshotId);
			await syncDirectoryIfSupported(stagingDir);
			await assertDirectoryIdentity(trustedRepositoryPath, repositoryIdentity);
			try {
				await createPrivateSqliteDirectory(snapshotDir);
				snapshotDirectoryCreated = true;
			} catch (error) {
				if (error.code === "EEXIST") throw new Error(`SQLite snapshot directory already exists: ${snapshotDir}`, { cause: error });
				throw error;
			}
			await assertDirectoryIdentity(trustedRepositoryPath, repositoryIdentity);
			publishedDirectory = await pinDirectory(snapshotDir, { label: "SQLite snapshot directory" });
			publishedIdentity = publishedDirectory.receipt.identity;
			applyPrivateModeSync(snapshotDir, SNAPSHOT_DIRECTORY_MODE);
			await assertPrivateStagingDirectory(publishedIdentity, snapshotDir);
			await publishedDirectory.assertCurrent();
			const pendingPath = path.join(snapshotDir, SNAPSHOT_PENDING_FILENAME);
			const pendingHandle = await fs$1.open(pendingPath, "wx+", SNAPSHOT_FILE_MODE);
			try {
				publishedEntries.set(SNAPSHOT_PENDING_FILENAME, await pendingHandle.stat());
				await pendingHandle.sync();
			} finally {
				await pendingHandle.close();
			}
			await publishedDirectory.assertCurrent();
			requireDirectorySync(await publishedDirectory.sync(), "SQLite snapshot directory");
			await assertDirectoryIdentity(trustedRepositoryPath, repositoryIdentity);
			requireDirectorySync(await syncDirectory(repositoryReceipt), "SQLite snapshot repository");
			await publishedDirectory.assertCurrent();
			await publishSnapshotEntryNoOverwrite(path.join(stagingDir, SNAPSHOT_SQLITE_FILENAME), path.join(snapshotDir, SNAPSHOT_SQLITE_FILENAME), SNAPSHOT_SQLITE_FILENAME, publishedEntries);
			await publishedDirectory.assertCurrent();
			await publishSnapshotEntryNoOverwrite(path.join(stagingDir, SNAPSHOT_MANIFEST_FILENAME), path.join(snapshotDir, SNAPSHOT_MANIFEST_FILENAME), SNAPSHOT_MANIFEST_FILENAME, publishedEntries);
			await publishedDirectory.assertCurrent();
			requireDirectorySync(await publishedDirectory.sync(), "SQLite snapshot directory");
			await assertPendingSnapshotContents(snapshotDir);
			const publishedManifest = await readSnapshotManifest(snapshotDir, snapshotId);
			if (!isDeepStrictEqual(publishedManifest, manifest)) throw new Error(`SQLite snapshot manifest changed during publication: ${snapshotDir}`);
			const publishedArtifact = await hashSnapshotArtifact(snapshotDir);
			const publishedArtifactPath = path.join(snapshotDir, SNAPSHOT_SQLITE_FILENAME);
			assertArtifactMatchesManifest(publishedArtifactPath, publishedArtifact, publishedManifest);
			await verifySnapshotDatabaseFile(publishedArtifactPath, publishedArtifact.stat, publishedManifest, trustedRepositoryPath);
			const expectedPendingIdentity = publishedEntries.get(SNAPSHOT_PENDING_FILENAME);
			const currentPendingIdentity = fs.lstatSync(pendingPath);
			if (!expectedPendingIdentity || !sameFileIdentity(expectedPendingIdentity, currentPendingIdentity)) throw new Error(`SQLite snapshot pending marker changed: ${pendingPath}`);
			await publishedDirectory.assertCurrent();
			fs.unlinkSync(pendingPath);
			requireDirectorySync(await publishedDirectory.sync(), "SQLite snapshot directory");
			await publishedDirectory.assertCurrent();
			const committedManifest = await readSnapshotManifest(snapshotDir, snapshotId);
			if (!isDeepStrictEqual(committedManifest, manifest)) throw new Error(`SQLite snapshot manifest changed after commit: ${snapshotDir}`);
			const committedArtifact = await hashSnapshotArtifact(snapshotDir);
			assertArtifactMatchesManifest(path.join(snapshotDir, SNAPSHOT_SQLITE_FILENAME), committedArtifact, committedManifest);
			await assertExactSnapshotContents(snapshotDir);
			await publishedDirectory.assertCurrent();
			await assertDirectoryIdentity(trustedRepositoryPath, repositoryIdentity);
			publishedEntries.delete(SNAPSHOT_PENDING_FILENAME);
			await publishedDirectory.close();
			publishedDirectory = void 0;
			return {
				ref: { path: snapshotRefPath },
				manifest
			};
		} catch (error) {
			await publishedDirectory?.close().catch(() => void 0);
			publishedDirectory = void 0;
			if (snapshotDirectoryCreated) publishedIdentity ??= await fs$1.lstat(snapshotDir).catch(() => void 0);
			if (publishedIdentity) {
				if (await removePublishedSnapshotDirectoryIfOwned(snapshotDir, publishedIdentity, publishedEntries)) await syncDirectoryIfSupported(trustedRepositoryPath);
			}
			throw error;
		} finally {
			if (stagingIdentity ? await removePrivateDirectoryIfOwned(stagingDir, stagingIdentity, SNAPSHOT_ARTIFACT_ENTRIES).catch(() => false) : await fs$1.rmdir(stagingDir).then(() => true).catch(() => false)) await syncDirectoryIfSupported(trustedRepositoryPath).catch(() => void 0);
		}
	}
	async verify(snapshot) {
		const snapshotDir = await this.#resolveSnapshotDirectory(snapshot);
		const manifest = await readVerifiedSnapshotManifest(snapshotDir);
		assertAllowedDatabaseRole(manifest, this.#allowedDatabaseRoles);
		const artifact = await hashSnapshotArtifact(snapshotDir);
		const artifactPath = path.join(snapshotDir, SNAPSHOT_SQLITE_FILENAME);
		assertArtifactMatchesManifest(artifactPath, artifact, manifest);
		await verifySnapshotDatabaseFile(artifactPath, artifact.stat, manifest, this.#validationRootPath);
		await assertExactSnapshotContents(snapshotDir);
		return {
			ok: true,
			manifest
		};
	}
	async restoreFresh(snapshot, targetPath) {
		const snapshotDir = await this.#resolveSnapshotDirectory(snapshot);
		const manifest = await readVerifiedSnapshotManifest(snapshotDir);
		assertAllowedDatabaseRole(manifest, this.#allowedDatabaseRoles);
		const resolvedTargetPath = path.resolve(targetPath);
		await assertFreshRestorePathsAbsent(resolvedTargetPath);
		const canonicalRepositoryPath = await fs$1.realpath(this.#repositoryPath);
		const canonicalRestoreParentPath = await canonicalPathFromExistingAncestor(path.dirname(resolvedTargetPath));
		const canonicalTargetPath = path.join(canonicalRestoreParentPath, path.basename(resolvedTargetPath));
		if (isPathInside(canonicalRepositoryPath, canonicalTargetPath)) throw new Error(`SQLite restore target must be outside snapshot repository ${this.#repositoryPath}: ${resolvedTargetPath}`);
		const restoreParentPath = path.dirname(canonicalTargetPath);
		const restoreParentReceipt = await ensureRestoreParentDirectory(restoreParentPath);
		const trustedRestoreParentPath = await fs$1.realpath(restoreParentPath);
		const trustedTargetPath = path.join(trustedRestoreParentPath, path.basename(resolvedTargetPath));
		if (!isPathInside(canonicalTargetPath, trustedTargetPath) || !isPathInside(trustedTargetPath, canonicalTargetPath)) throw new Error(`SQLite restore target changed while creating its parent: ${resolvedTargetPath}`);
		if (isPathInside(canonicalRepositoryPath, trustedTargetPath)) throw new Error(`SQLite restore target must be outside snapshot repository ${this.#repositoryPath}: ${resolvedTargetPath}`);
		const restoreParentIdentity = await fs$1.lstat(trustedRestoreParentPath);
		if (!sameFileIdentity(restoreParentReceipt.identity, restoreParentIdentity)) throw new Error(`SQLite restore parent changed after durable creation: ${trustedRestoreParentPath}`);
		await assertFreshRestorePathsAbsent(trustedTargetPath);
		return await withPrivateSqliteStagingDirectory({
			rootPath: trustedRestoreParentPath,
			expectedRootIdentity: restoreParentIdentity,
			prefix: ".tmp-restore-",
			allowedEntries: RESTORE_STAGING_ENTRIES,
			operation: async (stagingDir, stagingIdentity) => {
				const stagedSourcePath = path.join(stagingDir, SNAPSHOT_SQLITE_FILENAME);
				const stagedArtifact = await copySnapshotArtifact(snapshotDir, stagedSourcePath);
				await assertDirectoryIdentity(stagingDir, stagingIdentity);
				assertArtifactMatchesManifest(stagedSourcePath, stagedArtifact, manifest);
				await assertExactSnapshotContents(snapshotDir);
				await verifySnapshotDatabaseFile(stagedSourcePath, stagedArtifact.stat, manifest, trustedRestoreParentPath);
				await publishVerifiedSqliteFile({
					sourceIdentity: stagedArtifact.stat,
					sourcePath: stagedSourcePath,
					targetPath: trustedTargetPath,
					expectedContent: manifest.artifact,
					requireAtomicPublication: true,
					beforePublish: async () => {
						await assertDirectoryIdentity(trustedRestoreParentPath, restoreParentIdentity);
						await assertFreshRestorePathsAbsent(trustedTargetPath);
					},
					afterPublish: (guard) => {
						guard.assertTargetMatchesExpectedContent(() => {
							assertDirectoryIdentitySync(trustedRestoreParentPath, restoreParentIdentity);
							assertNoSqliteSidecarsSync(trustedTargetPath);
						});
					}
				});
				return {
					ok: true,
					manifest
				};
			}
		});
	}
	async list() {
		const repositoryStat = await lstatIfExists(this.#repositoryPath);
		if (!repositoryStat) return [];
		assertDirectory(repositoryStat, this.#repositoryPath, "SQLite snapshot repository");
		const entries = await fs$1.readdir(this.#repositoryPath, { withFileTypes: true });
		const snapshots = [];
		for (const entry of entries) {
			if (entry.name.startsWith(".tmp-")) {
				if (entry.isSymbolicLink() || !entry.isDirectory()) throw new Error(`SQLite snapshot repository contains unsafe staging entry: ${path.join(this.#repositoryPath, entry.name)}`);
				continue;
			}
			if (entry.isSymbolicLink() || !entry.isDirectory()) throw new Error(`SQLite snapshot repository contains unexpected entry: ${path.join(this.#repositoryPath, entry.name)}`);
			const snapshotPath = path.join(this.#repositoryPath, entry.name);
			const snapshotState = await classifySnapshotDirectory(snapshotPath);
			if (snapshotState === "incomplete") continue;
			const manifest = snapshotState === "complete-pending" ? await recoverCompletePendingSnapshot({
				allowedDatabaseRoles: this.#allowedDatabaseRoles,
				repositoryIdentity: repositoryStat,
				repositoryPath: this.#repositoryPath,
				snapshotPath,
				validationRootPath: this.#validationRootPath
			}) : await readVerifiedSnapshotManifest(snapshotPath);
			assertAllowedDatabaseRole(manifest, this.#allowedDatabaseRoles);
			snapshots.push({
				ref: { path: snapshotPath },
				manifest
			});
		}
		return snapshots.toSorted((left, right) => right.manifest.createdAt.localeCompare(left.manifest.createdAt) || right.manifest.snapshotId.localeCompare(left.manifest.snapshotId));
	}
	async #resolveSnapshotDirectory(snapshot) {
		const snapshotDir = path.resolve(snapshot.path);
		if (path.dirname(snapshotDir) !== this.#repositoryPath) throw new Error(`SQLite snapshot must be an immediate child of repository ${this.#repositoryPath}: ${snapshotDir}`);
		const repositoryStat = await lstatIfExists(this.#repositoryPath);
		if (!repositoryStat) throw new Error(`SQLite snapshot repository does not exist: ${this.#repositoryPath}. Check the snapshot path or create a snapshot with \`openclaw backup sqlite create\`.`);
		assertDirectory(repositoryStat, this.#repositoryPath, "SQLite snapshot repository");
		const snapshotStat = await lstatIfExists(snapshotDir);
		if (!snapshotStat) throw new Error(`SQLite snapshot does not exist: ${snapshotDir}. Run \`openclaw backup sqlite list --repository ${this.#repositoryPath}\` to inspect available snapshots.`);
		assertDirectory(snapshotStat, snapshotDir, "SQLite snapshot");
		if (await lstatIfExists(path.join(snapshotDir, SNAPSHOT_PENDING_FILENAME))) {
			if (await classifySnapshotDirectory(snapshotDir) === "complete-pending") await recoverCompletePendingSnapshot({
				allowedDatabaseRoles: this.#allowedDatabaseRoles,
				repositoryIdentity: repositoryStat,
				repositoryPath: this.#repositoryPath,
				snapshotPath: snapshotDir,
				validationRootPath: this.#validationRootPath
			});
		}
		return snapshotDir;
	}
};
async function readVerifiedSnapshotManifest(snapshotDir) {
	await assertExactSnapshotContents(snapshotDir);
	return await readSnapshotManifest(snapshotDir);
}
function assertArtifactMatchesManifest(artifactPath, artifact, manifest) {
	if (artifact.sizeBytes !== manifest.artifact.sizeBytes) throw new Error(`Snapshot artifact size mismatch for ${artifactPath}: expected ${manifest.artifact.sizeBytes}, got ${artifact.sizeBytes}`);
	if (artifact.sha256 !== manifest.artifact.sha256) throw new Error(`Snapshot artifact hash mismatch for ${artifactPath}: expected ${manifest.artifact.sha256}, got ${artifact.sha256}`);
}
function assertAllowedDatabaseRole(manifest, allowedRoles) {
	if (!allowedRoles || allowedRoles.includes(manifest.database.role)) return;
	throw new Error(`SQLite snapshot database role ${manifest.database.role} is not allowed for this operation.`);
}
async function verifySnapshotDatabaseFile(artifactPath, expectedIdentity, manifest, validationRootPath) {
	const beforeOpen = await fs$1.lstat(artifactPath);
	if (beforeOpen.isSymbolicLink() || !beforeOpen.isFile() || beforeOpen.nlink > 1 || !sameFileIdentity(expectedIdentity, beforeOpen)) throw new Error(`Snapshot artifact changed before SQLite verification: ${artifactPath}`);
	const validationRootIdentity = await lstatIfExists(validationRootPath);
	if (!validationRootIdentity) throw new Error(`SQLite validation root does not exist: ${validationRootPath}. Create a private directory there or pass an existing directory with \`--scratch\`.`);
	assertDirectory(validationRootIdentity, validationRootPath, "SQLite validation root");
	await withPrivateSqliteStagingDirectory({
		rootPath: validationRootPath,
		expectedRootIdentity: validationRootIdentity,
		prefix: ".tmp-verify-",
		allowedEntries: VALIDATION_STAGING_ENTRIES,
		operation: async (validationDir) => {
			const validationPath = path.join(validationDir, SNAPSHOT_SQLITE_FILENAME);
			const validationArtifact = await copySnapshotArtifact(path.dirname(artifactPath), validationPath);
			assertArtifactMatchesManifest(validationPath, validationArtifact, manifest);
			const database = openNodeSqliteDatabase(validationPath, {
				allowExtension: true,
				readOnly: true
			});
			try {
				database.exec("PRAGMA busy_timeout = 30000; PRAGMA trusted_schema = OFF;");
				await loadSqliteVecExtension({ db: database });
				assertSqliteIntegrity(database, artifactPath);
				buildManifestDatabaseValidator(manifest.database)(database, artifactPath);
			} finally {
				database.close();
			}
			const validatedArtifact = await hashSnapshotArtifact(validationDir);
			if (!sameFileIdentity(validationArtifact.stat, validatedArtifact.stat)) throw new Error(`Snapshot validation copy changed: ${validationPath}`);
			assertArtifactMatchesManifest(validationPath, validatedArtifact, manifest);
		}
	});
	const afterOpen = await fs$1.lstat(artifactPath);
	if (afterOpen.isSymbolicLink() || !afterOpen.isFile() || afterOpen.nlink > 1 || !sameFileIdentity(expectedIdentity, afterOpen)) throw new Error(`Snapshot artifact changed during SQLite verification: ${artifactPath}`);
	const verifiedArtifact = await hashSnapshotArtifact(path.dirname(artifactPath));
	if (!sameFileIdentity(expectedIdentity, verifiedArtifact.stat)) throw new Error(`Snapshot artifact changed after SQLite verification: ${artifactPath}`);
	assertArtifactMatchesManifest(artifactPath, verifiedArtifact, manifest);
}
function buildDatabaseManifest(identity, sourcePath, userVersion) {
	const basename = path.basename(sourcePath);
	if (identity.role === "global") return {
		role: "global",
		basename,
		userVersion
	};
	if (identity.role === "agent") return {
		role: "agent",
		agentId: identity.agentId,
		basename,
		userVersion
	};
	return {
		role: "generic",
		id: identity.id,
		basename,
		userVersion
	};
}
function buildManifestDatabaseValidator(manifest) {
	const validateOwner = buildSnapshotValidator(manifest);
	return (database, pathname) => {
		validateOwner(database, pathname);
		const userVersion = readSqliteUserVersion(database);
		if (userVersion !== manifest.userVersion) throw new Error(`Snapshot database user_version mismatch for ${pathname}: expected ${manifest.userVersion}, got ${userVersion}`);
	};
}
function buildSnapshotId(now) {
	return `${now.toISOString().replaceAll(/[:.]/g, "-")}-${randomUUID()}`;
}
async function ensurePrivateDirectory(directoryPath, scopeLabel) {
	let expectedExistingIdentity;
	if (process.platform !== "win32") try {
		const existingIdentity = await fs$1.lstat(directoryPath);
		assertDirectory(existingIdentity, directoryPath, scopeLabel);
		await assertTrustedStagingRoot(existingIdentity, directoryPath, { allowModeRepair: true });
		applyPrivateModeSync(directoryPath, SNAPSHOT_DIRECTORY_MODE);
		const repairedIdentity = await fs$1.lstat(directoryPath);
		if (!sameFileIdentity(existingIdentity, repairedIdentity)) throw new Error(`${scopeLabel} changed during private mode repair: ${directoryPath}`);
		expectedExistingIdentity = repairedIdentity;
	} catch (error) {
		if (error.code !== "ENOENT") throw error;
	}
	const receipt = await ensureDurableDirectory({
		directoryPath,
		label: scopeLabel,
		expectedExistingIdentity,
		create: async (targetPath) => {
			if (process.platform === "win32") {
				const parentResult = await ensureAbsoluteDirectory(path.dirname(targetPath), {
					mode: SNAPSHOT_DIRECTORY_MODE,
					scopeLabel
				});
				if (!parentResult.ok) throw parentResult.error;
				try {
					await createPrivateSqliteDirectory(targetPath);
					return;
				} catch (error) {
					if (error.code !== "EEXIST") throw error;
				}
			}
			const result = await ensureAbsoluteDirectory(targetPath, {
				mode: SNAPSHOT_DIRECTORY_MODE,
				scopeLabel
			});
			if (!result.ok) throw result.error;
			applyPrivateModeSync(result.path, SNAPSHOT_DIRECTORY_MODE);
		}
	});
	requireDirectorySync(receipt.parentSync, scopeLabel);
	return receipt;
}
async function ensureRestoreParentDirectory(directoryPath) {
	const receipt = await ensureDurableDirectory({
		directoryPath,
		label: "SQLite restore target",
		create: async (targetPath) => {
			const result = await ensureAbsoluteDirectory(targetPath, {
				mode: SNAPSHOT_DIRECTORY_MODE,
				scopeLabel: "SQLite restore target"
			});
			if (!result.ok) throw result.error;
		}
	});
	requireDirectorySync(receipt.parentSync, "SQLite restore target");
	return receipt;
}
function assertDirectory(stat, pathname, label) {
	if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error(`${label} must be a real directory: ${pathname}`);
}
async function assertDirectoryIdentity(directoryPath, expectedIdentity) {
	const currentIdentity = await fs$1.lstat(directoryPath);
	assertDirectory(currentIdentity, directoryPath, "SQLite staging directory");
	if (!sameFileIdentity(currentIdentity, expectedIdentity)) throw new Error(`SQLite staging directory changed during operation: ${directoryPath}`);
}
function assertDirectoryIdentitySync(directoryPath, expectedIdentity) {
	const currentIdentity = fs.lstatSync(directoryPath);
	assertDirectory(currentIdentity, directoryPath, "SQLite staging directory");
	if (!sameFileIdentity(currentIdentity, expectedIdentity)) throw new Error(`SQLite staging directory changed during operation: ${directoryPath}`);
}
async function publishSnapshotEntryNoOverwrite(sourcePath, targetPath, entryName, publishedEntries) {
	let publication;
	try {
		publication = await publishFileNoClobber(sourcePath, targetPath, {
			strategy: "link-or-copy",
			moveSource: true,
			durability: "fail-closed"
		});
	} catch (error) {
		const details = getPublishFileExclusiveFailureDetails(error);
		if (details?.targetCreated && details.cleanup !== "removed") {
			const [currentSource, currentTarget] = await Promise.all([fs$1.lstat(sourcePath).catch(() => void 0), fs$1.lstat(targetPath).catch(() => void 0)]);
			const matchesReceipt = details.targetIdentity && currentTarget && sameFileIdentity(details.targetIdentity, currentTarget);
			const matchesSource = currentSource && currentTarget && sameFileIdentity(currentSource, currentTarget);
			if (currentTarget && (matchesReceipt || matchesSource)) publishedEntries.set(entryName, currentTarget);
		}
		throw error;
	}
	const expectedTargetIdentity = publication.identity;
	publishedEntries.set(entryName, expectedTargetIdentity);
	const initialTargetIdentity = await fs$1.lstat(targetPath);
	if (!sameFileIdentity(expectedTargetIdentity, initialTargetIdentity)) throw new Error(`SQLite snapshot entry changed during publication: ${targetPath}`);
	const finalTargetIdentity = await fs$1.lstat(targetPath);
	if (!sameFileIdentity(initialTargetIdentity, finalTargetIdentity)) throw new Error(`SQLite snapshot entry changed after publication: ${targetPath}`);
	publishedEntries.set(entryName, finalTargetIdentity);
}
async function assertExactSnapshotContents(snapshotDir) {
	await assertSnapshotContents(snapshotDir, /* @__PURE__ */ new Set([SNAPSHOT_MANIFEST_FILENAME, SNAPSHOT_SQLITE_FILENAME]));
}
async function assertPendingSnapshotContents(snapshotDir) {
	await assertSnapshotContents(snapshotDir, /* @__PURE__ */ new Set([
		SNAPSHOT_MANIFEST_FILENAME,
		SNAPSHOT_PENDING_FILENAME,
		SNAPSHOT_SQLITE_FILENAME
	]));
}
async function assertSnapshotContents(snapshotDir, expected) {
	const entries = await fs$1.readdir(snapshotDir, { withFileTypes: true });
	for (const entry of entries) {
		if (!expected.delete(entry.name)) throw new Error(`SQLite snapshot contains unexpected entry: ${path.join(snapshotDir, entry.name)}`);
		if (entry.isSymbolicLink() || !entry.isFile()) throw new Error(`SQLite snapshot entry must be a regular file: ${path.join(snapshotDir, entry.name)}`);
		if ((await fs$1.lstat(path.join(snapshotDir, entry.name))).nlink > 1) throw new Error(`SQLite snapshot entry must not be hardlinked: ${path.join(snapshotDir, entry.name)}`);
	}
	if (expected.size > 0) throw new Error(`SQLite snapshot is missing ${[...expected].join(", ")}: ${snapshotDir}`);
}
async function classifySnapshotDirectory(snapshotDir) {
	const entries = await fs$1.readdir(snapshotDir, { withFileTypes: true });
	const knownEntries = /* @__PURE__ */ new Set([
		SNAPSHOT_MANIFEST_FILENAME,
		SNAPSHOT_PENDING_FILENAME,
		SNAPSHOT_SQLITE_FILENAME
	]);
	for (const entry of entries) if (!knownEntries.has(entry.name) || entry.isSymbolicLink() || !entry.isFile()) throw new Error(`SQLite snapshot contains unexpected incomplete entry: ${path.join(snapshotDir, entry.name)}`);
	const names = new Set(entries.map((entry) => entry.name));
	if (names.size === 0) return "incomplete";
	if (!names.has(SNAPSHOT_PENDING_FILENAME)) return "committed";
	return names.has("manifest.json") && names.has("database.sqlite") ? "complete-pending" : "incomplete";
}
async function recoverCompletePendingSnapshot(params) {
	const trustedRepositoryPath = await assertTrustedStagingRoot(params.repositoryIdentity, params.repositoryPath);
	await assertDirectoryIdentity(trustedRepositoryPath, params.repositoryIdentity);
	const snapshotDirectory = await pinDirectory(params.snapshotPath, { label: "SQLite pending snapshot directory" });
	try {
		const snapshotIdentity = snapshotDirectory.receipt.identity;
		await assertPrivateStagingDirectory(snapshotIdentity, params.snapshotPath);
		await snapshotDirectory.assertCurrent();
		if (await classifySnapshotDirectory(params.snapshotPath) === "incomplete") throw new Error(`SQLite snapshot is incomplete: ${params.snapshotPath}`);
		const manifest = await readSnapshotManifest(params.snapshotPath);
		assertAllowedDatabaseRole(manifest, params.allowedDatabaseRoles);
		const artifact = await hashSnapshotArtifact(params.snapshotPath);
		const artifactPath = path.join(params.snapshotPath, SNAPSHOT_SQLITE_FILENAME);
		assertArtifactMatchesManifest(artifactPath, artifact, manifest);
		await verifySnapshotDatabaseFile(artifactPath, artifact.stat, manifest, params.validationRootPath);
		requireDirectorySync(await snapshotDirectory.sync(), "SQLite pending snapshot directory");
		const pendingPath = path.join(params.snapshotPath, SNAPSHOT_PENDING_FILENAME);
		const pendingIdentity = lstatIfExistsSync(pendingPath);
		if (pendingIdentity) {
			if (pendingIdentity.isSymbolicLink() || !pendingIdentity.isFile() || pendingIdentity.nlink > 1) throw new Error(`SQLite snapshot pending marker is unsafe: ${pendingPath}`);
			await snapshotDirectory.assertCurrent();
			const currentPendingIdentity = lstatIfExistsSync(pendingPath);
			if (currentPendingIdentity) {
				if (!sameFileIdentity(pendingIdentity, currentPendingIdentity)) throw new Error(`SQLite snapshot pending marker changed: ${pendingPath}`);
				try {
					fs.unlinkSync(pendingPath);
				} catch (error) {
					if (error.code !== "ENOENT") throw error;
				}
			}
		}
		requireDirectorySync(await snapshotDirectory.sync(), "SQLite pending snapshot directory");
		await snapshotDirectory.assertCurrent();
		const committedManifest = await readVerifiedSnapshotManifest(params.snapshotPath);
		if (!isDeepStrictEqual(committedManifest, manifest)) throw new Error(`SQLite snapshot manifest changed during recovery: ${params.snapshotPath}`);
		assertArtifactMatchesManifest(artifactPath, await hashSnapshotArtifact(params.snapshotPath), committedManifest);
		await assertDirectoryIdentity(trustedRepositoryPath, params.repositoryIdentity);
		return committedManifest;
	} finally {
		await snapshotDirectory.close().catch(() => void 0);
	}
}
async function assertFreshRestorePathsAbsent(databasePath) {
	for (const candidate of [databasePath, ...SQLITE_SIDECAR_SUFFIXES.map((suffix) => `${databasePath}${suffix}`)]) if (await lstatIfExists(candidate)) throw new Error(`Fresh SQLite restore path already exists: ${candidate}`);
}
function assertNoSqliteSidecarsSync(databasePath) {
	for (const suffix of SQLITE_SIDECAR_SUFFIXES) {
		const sidecarPath = `${databasePath}${suffix}`;
		try {
			fs.lstatSync(sidecarPath);
		} catch (error) {
			if (error.code === "ENOENT") continue;
			throw error;
		}
		throw new Error(`Restored SQLite database has unexpected sidecar: ${sidecarPath}`);
	}
}
async function lstatIfExists(pathname) {
	try {
		return await fs$1.lstat(pathname);
	} catch (error) {
		if (error.code === "ENOENT") return;
		throw error;
	}
}
function lstatIfExistsSync(pathname) {
	try {
		return fs.lstatSync(pathname);
	} catch (error) {
		if (error.code === "ENOENT") return;
		throw error;
	}
}
async function removePrivateDirectoryIfOwned(directoryPath, expectedIdentity, allowedEntries) {
	const currentIdentity = await lstatIfExists(directoryPath);
	if (!currentIdentity) return false;
	if (currentIdentity.isSymbolicLink() || !currentIdentity.isDirectory() || !sameFileIdentity(currentIdentity, expectedIdentity)) throw new Error(`Private SQLite staging directory changed before cleanup: ${directoryPath}`);
	const entries = await fs$1.readdir(directoryPath, { withFileTypes: true });
	const verifiedPaths = [];
	for (const entry of entries) {
		const entryPath = path.join(directoryPath, entry.name);
		if (!allowedEntries.has(entry.name) || entry.isSymbolicLink() || !entry.isFile()) throw new Error(`Private SQLite staging directory has unexpected entry: ${entryPath}`);
		if ((await fs$1.lstat(entryPath)).nlink > 1) throw new Error(`Private SQLite staging file must not be hardlinked: ${entryPath}`);
		verifiedPaths.push(entryPath);
	}
	await Promise.all(verifiedPaths.map(async (entryPath) => await fs$1.unlink(entryPath)));
	await fs$1.rmdir(directoryPath);
	return true;
}
async function withPrivateSqliteStagingDirectory(options) {
	const trustedRootPath = await assertTrustedStagingRoot(options.expectedRootIdentity, options.rootPath);
	await assertDirectoryIdentity(trustedRootPath, options.expectedRootIdentity);
	const directoryPath = await createPrivateSqliteTempDirectory(trustedRootPath, options.prefix);
	const directoryIdentity = await fs$1.lstat(directoryPath);
	let outcome;
	try {
		applyPrivateModeSync(directoryPath, SNAPSHOT_DIRECTORY_MODE);
		await assertPrivateStagingDirectory(directoryIdentity, directoryPath);
		await assertDirectoryIdentity(trustedRootPath, options.expectedRootIdentity);
		outcome = {
			ok: true,
			value: await options.operation(directoryPath, directoryIdentity)
		};
	} catch (error) {
		outcome = {
			ok: false,
			error
		};
	}
	let cleanupOutcome;
	try {
		if (!await removePrivateDirectoryIfOwned(directoryPath, directoryIdentity, options.allowedEntries)) throw new Error(`Private SQLite staging directory disappeared: ${directoryPath}`);
		cleanupOutcome = { ok: true };
	} catch (error) {
		cleanupOutcome = {
			ok: false,
			error
		};
	}
	if (!cleanupOutcome.ok) {
		if (!outcome.ok) throw new AggregateError([outcome.error, cleanupOutcome.error], `SQLite staging operation and cleanup both failed: ${directoryPath}`);
		throw new Error(`Failed to clean private SQLite staging directory: ${directoryPath}`, { cause: cleanupOutcome.error });
	}
	requireDirectorySync(await syncDirectory({
		path: trustedRootPath,
		realPath: trustedRootPath,
		identity: options.expectedRootIdentity
	}), "Private SQLite staging root");
	if (!outcome.ok) throw outcome.error;
	return outcome.value;
}
async function assertTrustedStagingRoot(expectedIdentity, rootPath, options = {}) {
	const resolvedRootPath = path.resolve(rootPath);
	const trustedRootPath = await fs$1.realpath(resolvedRootPath);
	const rootIdentity = await fs$1.lstat(trustedRootPath);
	assertDirectory(rootIdentity, trustedRootPath, "Private SQLite staging root");
	if (!sameFileIdentity(rootIdentity, expectedIdentity)) throw new Error(`Private SQLite staging root changed during operation: ${resolvedRootPath}`);
	if (process.platform === "win32") {
		await assertTrustedWindowsStagingPath(trustedRootPath);
		return trustedRootPath;
	}
	const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
	const unsafeMode = (rootIdentity.mode & 18) !== 0;
	if (uid === void 0 || rootIdentity.uid !== uid || unsafeMode && options.allowModeRepair !== true) throw new Error(`Private SQLite staging root must be owned by the current user and not writable by other users: ${resolvedRootPath}`);
	if (process.platform === "darwin") await assertTrustedMacosAcl(trustedRootPath, options.allowModeRepair !== true);
	await assertTrustedPosixStagingAncestors(trustedRootPath, rootIdentity, uid);
	return trustedRootPath;
}
/** Create or strictly admit a Git repository through the local snapshot root trust policy. */
async function ensurePrivateSnapshotRepositoryRoot(rootPath) {
	try {
		return await assertTrustedStagingRoot(await fs$1.lstat(rootPath), rootPath);
	} catch (error) {
		if (error.code !== "ENOENT") throw error;
	}
	return await assertTrustedStagingRoot((await ensurePrivateDirectory(rootPath, "Git backup repository")).identity, rootPath);
}
async function assertPrivateStagingDirectory(expectedIdentity, directoryPath) {
	const currentIdentity = await fs$1.lstat(directoryPath);
	assertDirectory(currentIdentity, directoryPath, "Private SQLite staging directory");
	if (!sameFileIdentity(currentIdentity, expectedIdentity)) throw new Error(`Private SQLite staging directory changed during operation: ${directoryPath}`);
	if (process.platform === "win32") return;
	const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
	if (uid === void 0 || currentIdentity.uid !== uid || (currentIdentity.mode & 63) !== 0) throw new Error(`Private SQLite staging directory permissions are unsafe: ${directoryPath}`);
	if (process.platform === "darwin") await assertTrustedMacosAcl(directoryPath, true);
}
async function assertTrustedPosixStagingAncestors(rootPath, rootIdentity, uid) {
	let childIdentity = rootIdentity;
	let currentPath = path.dirname(rootPath);
	while (currentPath !== rootPath) {
		const currentIdentity = await fs$1.lstat(currentPath);
		assertDirectory(currentIdentity, currentPath, "SQLite staging ancestor");
		const writableByOtherUsers = (currentIdentity.mode & 18) !== 0;
		const ownerCanReplaceChild = currentIdentity.uid !== uid && currentIdentity.uid !== 0;
		const stickyOwnerIsTrusted = currentIdentity.uid === uid || currentIdentity.uid === 0;
		const stickyProtectsChild = (currentIdentity.mode & 512) !== 0 && stickyOwnerIsTrusted && childIdentity.uid === uid;
		if (ownerCanReplaceChild || writableByOtherUsers && !stickyProtectsChild) throw new Error(`SQLite staging ancestor must not allow another user to replace its child: ${currentPath}`);
		if (process.platform === "darwin") await assertTrustedMacosAcl(currentPath, false);
		const parentPath = path.dirname(currentPath);
		if (parentPath === currentPath) return;
		childIdentity = currentIdentity;
		currentPath = parentPath;
	}
}
function parseMacosAclEntries(output, pathname) {
	const lines = output.split(/\r?\n/u);
	const header = lines.shift();
	if (!header) throw new Error(`Unable to inspect macOS ACL for SQLite staging: ${pathname}`);
	const entries = [];
	for (const line of lines) {
		if (!/^\s*\d+:\s/u.test(line)) continue;
		const match = line.match(/^\s*\d+:\s+(.+?)\s+(?:inherited\s+)?(allow|deny)\s+([a-z_,]+)\s*$/u);
		if (!match) throw new Error(`Unable to parse macOS ACL for SQLite staging: ${pathname}`);
		const [, principal, effect, permissions] = match;
		if (!principal || !permissions || effect !== "allow" && effect !== "deny") throw new Error(`Unable to parse macOS ACL for SQLite staging: ${pathname}`);
		entries.push({
			principal: normalizeAclPrincipal(principal),
			effect,
			permissions: new Set(permissions.split(","))
		});
	}
	if (/^[^\s]{10}\+/u.test(header) && entries.length === 0) throw new Error(`Unable to parse macOS ACL for SQLite staging: ${pathname}`);
	return entries;
}
function normalizeAclPrincipal(principal) {
	return principal.trim().toLowerCase();
}
async function resolveTrustedMacosAclPrincipals() {
	macosTrustedAclPrincipalsPromise ??= (async () => {
		const dsmemberutil = resolveSystemBin("dsmemberutil");
		if (!dsmemberutil) throw new Error("Unable to resolve dsmemberutil for macOS ACL verification.");
		const currentUsername = os.userInfo().username;
		const usernames = /* @__PURE__ */ new Set([currentUsername, "root"]);
		const trusted = /* @__PURE__ */ new Set();
		for (const username of usernames) {
			const { stdout } = await runExec(dsmemberutil, [
				"getuuid",
				"-U",
				username
			], {
				timeoutMs: 5e3,
				maxBuffer: 65536
			});
			const uuid = stdout.trim();
			if (!/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/iu.test(uuid)) throw new Error(`Unable to resolve trusted macOS ACL principal for ${username}.`);
			trusted.add(normalizeAclPrincipal(uuid));
			trusted.add(normalizeAclPrincipal(username));
			trusted.add(normalizeAclPrincipal(`user:${username}`));
		}
		return trusted;
	})();
	return await macosTrustedAclPrincipalsPromise;
}
async function assertTrustedMacosAcl(pathname, requirePrivate) {
	const ls = resolveSystemBin("ls");
	if (!ls) throw new Error(`Unable to verify macOS ACL for SQLite staging: ${pathname}`);
	let entries;
	try {
		const [result, trustedPrincipals] = await Promise.all([runExec(ls, [
			"-lden",
			"--",
			pathname
		], {
			timeoutMs: 5e3,
			maxBuffer: 1048576
		}), resolveTrustedMacosAclPrincipals()]);
		entries = parseMacosAclEntries(result.stdout, pathname).filter((entry) => !trustedPrincipals.has(entry.principal));
	} catch (error) {
		throw new Error(`Unable to verify macOS ACL for SQLite staging: ${pathname}`, { cause: error });
	}
	if (entries.find((entry) => entry.effect === "allow" && (requirePrivate || [...entry.permissions].some((permission) => MACOS_REPLACEMENT_ACL_PERMISSIONS.has(permission))))) throw new Error(`macOS ACL permits untrusted SQLite staging access: ${pathname}`);
}
async function assertTrustedWindowsStagingPath(rootPath) {
	const paths = [rootPath];
	let currentPath = path.dirname(rootPath);
	while (currentPath !== rootPath) {
		paths.push(currentPath);
		const parentPath = path.dirname(currentPath);
		if (parentPath === currentPath) break;
		currentPath = parentPath;
	}
	let security;
	try {
		security = await inspectWindowsPathSecurity(paths);
	} catch (error) {
		throw new Error(`Unable to verify private Windows ACL for SQLite staging: ${rootPath}`, { cause: error });
	}
	if (security.paths.length !== paths.length) throw new Error(`Unable to verify private Windows ACL for SQLite staging: ${rootPath}`);
	for (const [index, pathname] of paths.entries()) {
		const pathSecurity = security.paths[index];
		if (!pathSecurity || path.resolve(pathSecurity.path) !== path.resolve(pathname)) throw new Error(`Unable to verify private Windows ACL for SQLite staging: ${pathname}`);
		assertTrustedWindowsAcl(pathname, index === 0, security.currentUserSid, pathSecurity);
	}
}
function assertTrustedWindowsAcl(pathname, requirePrivate, currentUserSid, security) {
	const pathRole = requirePrivate ? "repository root" : "ancestor";
	if (security.ownerSid !== currentUserSid && !WINDOWS_TRUSTED_OWNER_SIDS.has(security.ownerSid)) throw new Error(`Windows SQLite staging ${pathRole} is owned by an untrusted principal: path=${pathname} principal=${security.ownerSid}. Choose a local directory owned only by the current user or a trusted OS principal.`);
	const allowedEntries = security.entries.filter((entry) => entry.accessType === "Allow");
	if (allowedEntries.length === 0) throw new Error(`Unable to verify private Windows ACL for SQLite staging: ${pathname}`);
	const unsafeEntry = allowedEntries.filter((entry) => entry.principal !== currentUserSid && !WINDOWS_TRUSTED_ACCESS_SIDS.has(entry.principal)).map(windowsSecurityEntryToAclEntry).find((entry) => windowsAclEntryPermitsUnsafeStagingAccess(entry, requirePrivate));
	if (unsafeEntry) throw new Error(`Windows ACL permits untrusted SQLite staging access on ${pathRole}: path=${pathname} principal=${unsafeEntry.principal} rights=${unsafeEntry.rawRights}. Remove the untrusted grant or choose a private local directory; do not use a shared or synced root.`);
}
function windowsSecurityEntryToAclEntry(entry) {
	const rights = WINDOWS_FILE_RIGHTS.filter(([right]) => (entry.rightsMask & right) !== 0).map(([, name]) => name);
	if ((entry.rightsMask & ~WINDOWS_KNOWN_FILE_RIGHTS_MASK) !== 0) rights.push("UNKNOWN");
	const inheritanceFlags = new Set(entry.inheritanceFlags.split(",").map((flag) => flag.trim()));
	const propagationFlags = new Set(entry.propagationFlags.split(",").map((flag) => flag.trim()));
	const rawFlags = [
		inheritanceFlags.has("ObjectInherit") ? "(OI)" : "",
		inheritanceFlags.has("ContainerInherit") ? "(CI)" : "",
		propagationFlags.has("NoPropagateInherit") ? "(NP)" : "",
		propagationFlags.has("InheritOnly") ? "(IO)" : ""
	].join("");
	return {
		principal: entry.principal,
		rights,
		rawRights: `${rawFlags}(${rights.join(",")})`,
		canRead: (entry.rightsMask & WINDOWS_READ_RIGHTS_MASK) !== 0,
		canWrite: (entry.rightsMask & WINDOWS_WRITE_RIGHTS_MASK) !== 0
	};
}
function windowsAclEntryPermitsUnsafeStagingAccess(entry, requirePrivate) {
	if (!requirePrivate && /\(IO\)/iu.test(entry.rawRights)) return false;
	const rights = entry.rights.map((right) => right.toUpperCase());
	const unsafeRights = requirePrivate ? WINDOWS_STAGING_ACCESS_RIGHTS : WINDOWS_STAGING_REPLACEMENT_RIGHTS;
	return requirePrivate && (entry.canWrite || entry.canRead) || rights.some((right) => unsafeRights.has(right));
}
async function inspectWindowsPathSecurity(pathnames) {
	const stdout = await runEncodedWindowsPowerShell([
		"$ErrorActionPreference = 'Stop'",
		`$paths = ConvertFrom-Json ([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${Buffer.from(JSON.stringify(pathnames), "utf8").toString("base64")}')))`,
		"$pathSecurity = @($paths | ForEach-Object { $path = [string]$_; $acl = Get-Acl -LiteralPath $path; $entries = @($acl.Access | ForEach-Object { $identity = $_.IdentityReference; try { $principal = $identity.Translate([System.Security.Principal.SecurityIdentifier]).Value } catch { $principal = [string]$identity.Value }; $rightsMask = ([int64][int32]$_.FileSystemRights) -band 0xffffffffL; [pscustomobject]@{ principal = $principal; accessType = [string]$_.AccessControlType; rightsMask = $rightsMask; inheritanceFlags = [string]$_.InheritanceFlags; propagationFlags = [string]$_.PropagationFlags } }); [pscustomobject]@{ path = $path; ownerSid = $acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value; entries = $entries } })",
		"$payload = [pscustomobject]@{ currentUserSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value; paths = $pathSecurity }",
		"$json = ConvertTo-Json -InputObject $payload -Compress -Depth 4",
		"[Console]::Out.Write([Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($json)))"
	].join("; "), WINDOWS_ACL_METADATA_MAX_BUFFER);
	let parsed;
	try {
		parsed = JSON.parse(Buffer.from(stdout.trim(), "base64").toString("utf8"));
	} catch (error) {
		throw new Error("Unable to parse Windows ACL metadata.", { cause: error });
	}
	const result = WINDOWS_PATH_SECURITY_SCHEMA.safeParse(parsed);
	if (!result.success) throw new Error("Invalid Windows ACL metadata.", { cause: result.error });
	return result.data;
}
async function runEncodedWindowsPowerShell(command, maxBuffer) {
	const powershell = resolveSystemBin("powershell");
	if (!powershell) throw new Error("Unable to resolve PowerShell for Windows SQLite path security.");
	try {
		const { stdout } = await runExec(powershell, buildEncodedPowerShellArgs(command), {
			env: { PSModulePath: path.win32.join(path.win32.dirname(powershell), "Modules") },
			timeoutMs: WINDOWS_POWERSHELL_COLD_SPAWN_TIMEOUT_MS,
			maxBuffer
		});
		return stdout;
	} catch (error) {
		throw buildPowerShellFailureCause(error);
	}
}
async function removePublishedSnapshotDirectoryIfOwned(directoryPath, expectedIdentity, publishedEntries) {
	const currentIdentity = await lstatIfExists(directoryPath);
	if (!currentIdentity || currentIdentity.isSymbolicLink() || !currentIdentity.isDirectory() || !sameFileIdentity(currentIdentity, expectedIdentity)) return false;
	const entries = await fs$1.readdir(directoryPath, { withFileTypes: true });
	for (const entry of entries) {
		const expectedEntryIdentity = publishedEntries.get(entry.name);
		if (!expectedEntryIdentity || entry.isSymbolicLink() || !entry.isFile()) continue;
		const entryPath = path.join(directoryPath, entry.name);
		const currentEntryIdentity = await fs$1.lstat(entryPath);
		if (sameFileIdentity(currentEntryIdentity, expectedEntryIdentity)) await fs$1.unlink(entryPath);
	}
	if ((await fs$1.readdir(directoryPath)).length > 0) return false;
	await fs$1.rmdir(directoryPath);
	return true;
}
//#endregion
//#region src/snapshot/git-backup.ts
const GIT_BACKUP_MATERIALIZE_MAX_BYTES = 1073741824;
const GIT_BACKUP_DIAGNOSTIC_MAX_LENGTH = 500;
const GIT_BACKUP_NON_BACKUP_HISTORY_WARNING = "repository history contains non-backup commits; use a dedicated backup repository";
function redactGitBackupText(value) {
	return value.split("\n").map((line) => redactSensitiveUrlLikeString(line)).join("\n");
}
function sanitizeGitBackupDiagnostic(value) {
	return truncateUtf16Safe(redactGitBackupText(value), GIT_BACKUP_DIAGNOSTIC_MAX_LENGTH);
}
function formatGitBackupCommandResult(command, result) {
	const redacted = {
		...result,
		stderr: redactGitBackupText(result.stderr),
		stdout: redactGitBackupText(result.stdout)
	};
	const header = formatCommandResult(command, {
		...redacted,
		stderr: "",
		stdout: ""
	});
	const streams = ["stderr", "stdout"].flatMap((stream) => {
		const output = formatCommandOutput(redacted[stream]);
		return output ? [{
			stream,
			output
		}] : [];
	});
	const fixedLength = header.length + streams.reduce((total, { stream }) => total + 1 + `${stream}: `.length, 0);
	if (streams.length === 0 || fixedLength >= 1200) return truncateUtf16Safe(header, BACKUP_RUN_ERROR_MAX_LENGTH);
	const outputBudget = BACKUP_RUN_ERROR_MAX_LENGTH - fixedLength;
	const lengths = streams.map(({ output }) => output.length);
	const first = Math.min(lengths[0] ?? 0, Math.max(Math.ceil(outputBudget / 2), outputBudget - (lengths[1] ?? 0)));
	const allocations = [first, Math.min(lengths[1] ?? 0, outputBudget - first)];
	const fit = (output, maxLength) => {
		if (output.length <= maxLength) return output;
		if (maxLength <= 1) return truncateUtf16Safe("…", maxLength);
		const source = output.startsWith("…\n") ? output.slice(2) : output;
		return `…\n${sliceUtf16Safe(source, Math.max(0, source.length - (maxLength - 2)))}`;
	};
	return [header, ...streams.map(({ stream, output }, index) => `${stream}: ${fit(output, allocations[index] ?? 0)}`)].join("\n");
}
function gitBackupRepositoryPrivacyRemediation(repositoryPath, cause) {
	if (process.platform === "win32") return `${cause instanceof Error && cause.message ? ` ${sanitizeGitBackupDiagnostic(cause.message)}` : ""} Remove non-user ACL grants from ${repositoryPath} or choose a private local directory. Do not use a shared or synced folder for SQLite backups.`;
	return `Fix its ownership and run chmod 700 ${repositoryPath}.`;
}
async function assertGitRepository(repositoryPath, env) {
	const topLevel = await requireGitCommand(repositoryPath, ["rev-parse", "--show-toplevel"], { env });
	const [canonicalTopLevel, canonicalRepository] = await Promise.all([fs$1.realpath(topLevel), fs$1.realpath(repositoryPath)]);
	if (canonicalTopLevel !== canonicalRepository) throw new Error(`Backup repository must be the Git worktree root: ${repositoryPath}`);
}
/** Initialize or adopt an operator-owned Git backup repository. */
async function initializeGitBackupRepository(params) {
	const repositoryPath = path.resolve(params.repositoryPath);
	const stateDir = path.resolve(params.stateDir);
	const [canonicalRepositoryPath, canonicalStateDir] = await Promise.all([canonicalPathFromExistingAncestor(repositoryPath), canonicalPathFromExistingAncestor(stateDir)]);
	if (isPathInside(canonicalStateDir, canonicalRepositoryPath) || isPathInside(canonicalRepositoryPath, canonicalStateDir)) throw new Error(`Git backup repository must be outside the OpenClaw state directory: ${stateDir}`);
	try {
		await ensurePrivateSnapshotRepositoryRoot(repositoryPath);
	} catch (error) {
		throw new Error(`Git backup repository must be owned by the current user and not writable by other users: ${repositoryPath}. ${gitBackupRepositoryPrivacyRemediation(repositoryPath, error)}`, { cause: error });
	}
	if ((await executeGitCommand(repositoryPath, ["rev-parse", "--show-toplevel"], { env: params.gitEnv })).code !== 0) await requireGitCommand(repositoryPath, ["init"], { env: params.gitEnv });
	await assertGitRepository(repositoryPath, params.gitEnv);
	const remote = params.remote?.trim();
	if (remote) {
		const existing = await executeGitCommand(repositoryPath, [
			"remote",
			"get-url",
			"origin"
		], { env: params.gitEnv });
		if (existing.code === 0 && existing.stdout.trim() !== remote) throw new Error(`Git backup repository already has a different origin: ${sanitizeGitBackupDiagnostic(existing.stdout.trim())}`);
		if (existing.code !== 0) await requireGitCommand(repositoryPath, [
			"remote",
			"add",
			"origin",
			remote
		], { env: params.gitEnv });
	}
	return { repositoryPath };
}
async function isBackupOwnedScope(scopePath) {
	const identity = await fs$1.lstat(scopePath).catch((error) => error.code === "ENOENT" ? void 0 : null);
	if (identity === void 0) return true;
	if (!identity?.isDirectory()) return false;
	try {
		if ((await fs$1.readdir(scopePath)).length === 0) return true;
		parseGitBackupManifest(await fs$1.readFile(path.join(scopePath, GIT_BACKUP_MANIFEST), "utf8"), scopePath);
		return true;
	} catch {
		return false;
	}
}
async function assertBackupOwnedScope(scopePath) {
	if (!await isBackupOwnedScope(scopePath)) throw new Error(`Refusing to replace non-backup-owned path ${scopePath}; the repository must be dedicated to OpenClaw backups.`);
}
async function removeStaleAgentScopes(repositoryPath) {
	const agentsPath = path.join(repositoryPath, "agents");
	let entries;
	try {
		entries = await fs$1.readdir(agentsPath);
	} catch (error) {
		if (error.code === "ENOENT") return;
		throw error;
	}
	const scopes = entries.map((entry) => path.join(agentsPath, entry));
	await Promise.all(scopes.map(async (scope) => await assertBackupOwnedScope(scope)));
	await Promise.all(scopes.map(async (scope) => await fs$1.rm(scope, { recursive: true })));
}
async function copyStagedScope(stagingRoot, repositoryPath, identity) {
	const relative = gitBackupScopePath(identity);
	const source = path.join(stagingRoot, relative);
	const target = path.join(repositoryPath, relative);
	await assertBackupOwnedScope(target);
	await fs$1.rm(target, {
		recursive: true,
		force: true
	});
	await fs$1.mkdir(path.dirname(target), {
		recursive: true,
		mode: 448
	});
	await fs$1.cp(source, target, {
		recursive: true,
		force: false
	});
}
async function commitGitBackup(params) {
	const email = await executeGitCommand(params.repositoryPath, [
		"config",
		"--get",
		"user.email"
	], { env: params.env });
	const identityArgs = email.code === 0 && email.stdout.trim() ? [] : [
		"-c",
		"user.name=OpenClaw",
		"-c",
		"user.email=backup@openclaw.local"
	];
	await requireGitCommand(params.repositoryPath, [
		...identityArgs,
		"commit",
		"-m",
		params.message,
		"--",
		...params.scopes
	], { env: params.env });
	return await requireGitCommand(params.repositoryPath, ["rev-parse", "HEAD"], { env: params.env });
}
/** Snapshot selected databases, update the deterministic tree, and commit one Git revision. */
async function createGitBackup(params) {
	const repositoryPath = path.resolve(params.repositoryPath);
	await initializeGitBackupRepository({
		repositoryPath,
		stateDir: params.stateDir,
		gitEnv: params.gitEnv
	});
	const stagingRoot = await fs$1.mkdtemp(path.join(os.tmpdir(), "openclaw-git-backup-"));
	await fs$1.chmod(stagingRoot, 448);
	const manifests = [];
	try {
		for (const database of params.databases) {
			const outputPath = path.join(stagingRoot, gitBackupScopePath(database.identity));
			await fs$1.mkdir(path.dirname(outputPath), {
				recursive: true,
				mode: 448
			});
			const copyPath = path.join(stagingRoot, `${database.identity.role}-${manifests.length}.sqlite`);
			await createOpenClawSnapshotCopy({
				database,
				targetPath: copyPath
			});
			manifests.push(await dumpGitBackupDatabase({
				snapshotPath: copyPath,
				outputPath,
				identity: database.identity,
				excludeSecrets: params.excludeSecrets
			}));
			await fs$1.rm(copyPath, { force: true });
		}
		if (params.all) await removeStaleAgentScopes(repositoryPath);
		for (const database of params.databases) await copyStagedScope(stagingRoot, repositoryPath, database.identity);
	} finally {
		await fs$1.rm(stagingRoot, {
			recursive: true,
			force: true
		}).catch(() => void 0);
	}
	await Promise.all(["global", "agents"].map(async (scope) => fs$1.mkdir(path.join(repositoryPath, scope), {
		recursive: true,
		mode: 448
	})));
	await requireGitCommand(repositoryPath, [
		"add",
		"-A",
		"--",
		"global",
		"agents"
	], { env: params.gitEnv });
	const changed = await requireGitCommand(repositoryPath, [
		"status",
		"--porcelain",
		"--",
		"global",
		"agents"
	], { env: params.gitEnv });
	let commit;
	if (changed) {
		const now = params.now ?? /* @__PURE__ */ new Date();
		if (!Number.isFinite(now.getTime())) throw new Error("Git backup timestamp is invalid.");
		const stagedBackupPaths = await requireGitCommand(repositoryPath, [
			"diff",
			"--cached",
			"--name-only",
			"--",
			"global",
			"agents"
		], { env: params.gitEnv });
		const commitScopes = ["global", "agents"].filter((scope) => stagedBackupPaths.split("\n").some((entry) => entry.startsWith(`${scope}/`)));
		commit = await commitGitBackup({
			repositoryPath,
			message: `openclaw backup ${now.toISOString()}`,
			scopes: commitScopes,
			env: params.gitEnv
		});
	}
	let pushed = false;
	let pushWarning;
	if (params.push) {
		if (await requireGitCommand(repositoryPath, [
			"rev-list",
			"HEAD",
			"--invert-grep",
			"--grep=^openclaw backup ",
			"--count"
		], { env: params.gitEnv }) !== "0") pushWarning = GIT_BACKUP_NON_BACKUP_HISTORY_WARNING;
		else {
			const pushedResult = await executeGitCommand(repositoryPath, [
				"push",
				"-u",
				"origin",
				"HEAD"
			], { env: params.gitEnv });
			if (pushedResult.code === 0) pushed = true;
			else pushWarning = formatGitBackupCommandResult("git push", pushedResult);
		}
	}
	return {
		repositoryPath,
		...commit ? { commit } : {},
		noChanges: !changed,
		pushed,
		...pushWarning ? { pushWarning } : {},
		manifests
	};
}
async function resolveGitCommit(repositoryPath, ref) {
	return await requireGitCommand(repositoryPath, [
		"rev-parse",
		"--verify",
		`${ref?.trim() || "HEAD"}^{commit}`
	]);
}
/** Materialize one database scope from a Git ref into a private temporary directory. */
async function materializeGitBackupRef(params) {
	const repositoryPath = path.resolve(params.repositoryPath);
	await assertGitRepository(repositoryPath);
	const commit = await resolveGitCommit(repositoryPath, params.ref);
	const scope = gitBackupScopePath(params.identity).split(path.sep).join("/");
	const files = (await requireGitCommand(repositoryPath, [
		"ls-tree",
		"-r",
		"--name-only",
		commit,
		"--",
		scope
	])).split("\n").filter(Boolean);
	if ([.../* @__PURE__ */ new Set([`${scope}/manifest.json`, `${scope}/schema.sql`])].some((entry) => !files.includes(entry))) throw new Error(`Git backup ref ${commit} does not contain ${scope}.`);
	const root = await fs$1.mkdtemp(path.join(os.tmpdir(), "openclaw-git-restore-"));
	await fs$1.chmod(root, 448);
	const outputPath = path.join(root, scope);
	try {
		for (const file of files) {
			if (file !== `${scope}/manifest.json` && file !== `${scope}/schema.sql` && !file.startsWith(`${scope}/tables/`)) throw new Error(`Git backup ref contains an unexpected file: ${file}`);
			const relative = file.slice(scope.length + 1);
			const destination = path.join(outputPath, relative);
			await fs$1.mkdir(path.dirname(destination), {
				recursive: true,
				mode: 448
			});
			await fs$1.writeFile(destination, await requireGitCommandBuffer(repositoryPath, ["show", `${commit}:${file}`], { maxOutputBytes: GIT_BACKUP_MATERIALIZE_MAX_BYTES }), { mode: 384 });
		}
		return {
			commit,
			path: outputPath,
			cleanup: async () => await fs$1.rm(root, {
				recursive: true,
				force: true
			})
		};
	} catch (error) {
		await fs$1.rm(root, {
			recursive: true,
			force: true
		}).catch(() => void 0);
		throw error;
	}
}
/** Restore one database from a Git ref to a caller-selected fresh path. */
async function restoreGitBackupRef(params) {
	const materialized = await materializeGitBackupRef(params);
	try {
		return {
			...await restoreGitBackupDirectory({
				sourcePath: materialized.path,
				targetPath: params.targetPath,
				expectedIdentity: params.identity
			}),
			commit: materialized.commit
		};
	} finally {
		await materialized.cleanup();
	}
}
/** Verify a Git snapshot by restoring it privately and comparing every table digest. */
async function verifyGitBackupRef(params) {
	const scratch = await fs$1.mkdtemp(path.join(os.tmpdir(), "openclaw-git-verify-"));
	await fs$1.chmod(scratch, 448);
	try {
		return await restoreGitBackupRef({
			...params,
			targetPath: path.join(scratch, "database.sqlite")
		});
	} finally {
		await fs$1.rm(scratch, {
			recursive: true,
			force: true
		}).catch(() => void 0);
	}
}
/** Return bounded Git backup log entries for CLI rendering. */
async function readGitBackupLog(params) {
	await assertGitRepository(params.repositoryPath);
	const symbolicHead = await executeGitCommand(params.repositoryPath, [
		"symbolic-ref",
		"--quiet",
		"HEAD"
	]);
	if (symbolicHead.code === 0) {
		const headRef = symbolicHead.stdout.trim();
		const headExists = await executeGitCommand(params.repositoryPath, [
			"show-ref",
			"--verify",
			"--quiet",
			headRef
		]);
		if (headExists.code === 1 && headRef.startsWith("refs/heads/")) return [];
		if (headExists.code !== 0) throw new Error(formatGitBackupCommandResult("git show-ref HEAD", headExists));
	} else if (symbolicHead.code !== 1) throw new Error(formatGitBackupCommandResult("git symbolic-ref HEAD", symbolicHead));
	const result = await executeGitCommand(params.repositoryPath, [
		"log",
		`--max-count=${params.limit}`,
		"--pretty=format:%H%x09%cI%x09%s"
	]);
	if (result.code !== 0) throw new Error(formatGitBackupCommandResult("git log", result));
	return result.stdout.split("\n").filter(Boolean).map((line) => {
		const [commit = "", date = "", ...message] = line.split("	");
		return {
			commit,
			date,
			message: message.join("	")
		};
	});
}
//#endregion
//#region src/commands/backup-git.ts
const GIT_BACKUP_PUSH_CREDENTIAL_WARNING = "Warning: pushed backup history contains credential material; keep the Git remote private.";
async function resolveCreateDatabases(runtime, options) {
	const normalizedAgents = [...new Set((options.agents ?? []).map((agent) => {
		const trimmed = agent.trim();
		if (!trimmed) throw new Error("--agent must not be blank");
		return normalizeAgentId(trimmed);
	}))];
	const explicit = options.global === true || normalizedAgents.length > 0;
	if (options.all && explicit) throw new Error("Use --all by itself, or select --global and --agent scopes explicitly.");
	if (!options.all && !explicit) throw new Error("Choose at least one Git backup scope: --all, --global, or --agent <id>.");
	let agents = [];
	if (options.all || normalizedAgents.length > 0) {
		const config = getRuntimeConfig({ skipPluginValidation: true });
		const agentIds = options.all ? listAgentIds(config).toSorted() : normalizedAgents.map((agent) => resolveConfiguredAgentId(config, agent));
		agents = await Promise.all(agentIds.map((agentId) => resolveBackupAgentRoot(config, agentId)));
	}
	const databases = [];
	if (options.all || options.global) databases.push({
		path: await fs$1.realpath(resolveOpenClawStateSqlitePath()),
		identity: { role: "global" }
	});
	for (const { agentId, databasePath } of agents) {
		let resolvedPath;
		try {
			resolvedPath = await fs$1.realpath(databasePath);
		} catch (error) {
			if (options.all && error.code === "ENOENT") {
				runtime.error(`Warning: skipping agent ${agentId}: no database at ${databasePath}`);
				continue;
			}
			throw error;
		}
		databases.push({
			path: resolvedPath,
			identity: {
				role: "agent",
				agentId
			}
		});
	}
	if (databases.length === 0) throw new Error("No Git backup databases were found for the selected scope.");
	return databases;
}
function resolveOneIdentity(options) {
	const agent = options.agent?.trim();
	if (options.global === true && agent) throw new Error("Choose exactly one Git backup scope: --global or --agent <id>.");
	if (options.global !== true && !agent) throw new Error("Choose a Git backup scope: --global or --agent <id>.");
	return options.global === true ? { role: "global" } : {
		role: "agent",
		agentId: normalizeAgentId(agent)
	};
}
async function backupGitInitCommand(runtime, options) {
	const result = await initializeGitBackupRepository({
		repositoryPath: resolveRequiredBackupPath(options.repository, "--repository"),
		stateDir: resolveStateDir(),
		remote: options.remote
	});
	if (options.json) writeRuntimeJson(runtime, result);
	else runtime.log(`Git backup repository ready: ${shortenHomePath(result.repositoryPath)}`);
	return result;
}
async function backupGitCreateCommand(runtime, options) {
	const repositoryPath = resolveRequiredBackupPath(options.repository, "--repository");
	if (options.push && !options.excludeSecrets) runtime.error(GIT_BACKUP_PUSH_CREDENTIAL_WARNING);
	try {
		const result = await createGitBackup({
			repositoryPath,
			stateDir: resolveStateDir(),
			databases: await resolveCreateDatabases(runtime, options),
			all: options.all,
			excludeSecrets: options.excludeSecrets,
			push: options.push
		});
		recordBackupOutcomeBestEffort(runtime, {
			kind: "git",
			archivePath: repositoryPath,
			status: "ok",
			target: result.commit,
			error: result.pushWarning,
			...result.pushWarning ? { pushFailed: true } : {}
		});
		if (options.json) writeRuntimeJson(runtime, result);
		else if (result.noChanges) runtime.log(`Git backup: no changes (${shortenHomePath(repositoryPath)})`);
		else runtime.log(`Git backup committed: ${result.commit}`);
		if (result.pushWarning) runtime.error(`Warning: Git backup committed, but push failed: ${result.pushWarning}`);
		return result;
	} catch (error) {
		recordBackupOutcomeBestEffort(runtime, {
			kind: "git",
			archivePath: repositoryPath,
			status: "failed",
			error: formatErrorMessage(error)
		});
		throw error;
	}
}
async function backupGitLogCommand(runtime, options) {
	const repositoryPath = resolveRequiredBackupPath(options.repository, "--repository");
	const limit = options.limit ?? 20;
	if (!Number.isSafeInteger(limit) || limit < 1) throw new Error("--limit must be a positive integer.");
	const entries = await readGitBackupLog({
		repositoryPath,
		limit
	});
	if (options.json) writeRuntimeJson(runtime, {
		repositoryPath,
		entries
	});
	else if (entries.length === 0) runtime.log(`No Git backup commits in ${shortenHomePath(repositoryPath)}.`);
	else runtime.log(entries.map((entry) => `${entry.commit}\t${entry.date}\t${entry.message}`).join("\n"));
	return entries;
}
async function backupGitVerifyCommand(runtime, options) {
	const result = await verifyGitBackupRef({
		repositoryPath: resolveRequiredBackupPath(options.repository, "--repository"),
		identity: resolveOneIdentity(options),
		ref: options.ref
	});
	if (options.json) writeRuntimeJson(runtime, result);
	else {
		for (const table of result.tables) runtime.log(`${table.ok ? "ok" : "failed"}\t${table.table}\t${table.rows}\t${table.sha256}`);
		runtime.log(`Git backup verified: ${result.commit}`);
	}
	return result;
}
async function backupGitRestoreCommand(runtime, options) {
	const result = await restoreGitBackupRef({
		repositoryPath: resolveRequiredBackupPath(options.repository, "--repository"),
		identity: resolveOneIdentity(options),
		ref: options.ref,
		targetPath: resolveRequiredBackupPath(options.target, "--target")
	});
	if (options.json) writeRuntimeJson(runtime, result);
	else {
		runtime.log(`Git backup restored: ${shortenHomePath(result.targetPath)} (${result.commit})`);
		if (result.excludedTables.length > 0) runtime.error(`Warning: this redacted backup omits tables: ${result.excludedTables.join(", ")}`);
		if (result.excludedConfigStateKeyPrefixes.length > 0) runtime.error(`Warning: this redacted backup omits machine-state values under: ${result.excludedConfigStateKeyPrefixes.join(", ")}`);
	}
	return result;
}
//#endregion
//#region src/commands/backup-restore.ts
const BACKUP_RESTORE_WARNINGS = [
	"Restoring an archive is time travel: every restored state surface rolls back to the archive timestamp.",
	"Messaging-channel credentials with ratchet state, especially WhatsApp, may desynchronize after rollback and require relinking.",
	"Approvals and delivery/dedupe state also roll back; review pending approvals before resuming the Gateway.",
	"Plugin node_modules are not archived; after activation, run `openclaw plugins update <id>` or reinstall with `openclaw plugins install <spec> --force`.",
	"Generated plugin-skills links are not archived; after activation, run `openclaw skills list` or start an agent session to rebuild them."
];
async function assertTargetOutsideLiveState(targetPath) {
	const [canonicalTarget, canonicalStateDir] = await Promise.all([canonicalizePathForContainment(targetPath), canonicalizePathForContainment(resolveStateDir())]);
	if (isPathWithin(canonicalTarget, canonicalStateDir)) throw new Error(`Backup restore target must be outside the live OpenClaw state directory: ${targetPath}`);
	const configSnapshot = await readConfigFileSnapshot({ observe: false });
	const discoverySnapshot = resolveStartupConfigSnapshot(configSnapshot);
	if (!discoverySnapshot) return;
	const agentRoots = await resolveBackupAgentRoots(discoverySnapshot.config);
	for (const { sourcePath } of agentRoots) if (isPathWithin(canonicalTarget, sourcePath)) throw new Error(`Backup restore target must be outside the live OpenClaw agent directory: ${targetPath}`);
}
async function prepareRestoreTarget(targetPath) {
	try {
		if (!(await fs$1.lstat(targetPath)).isDirectory()) throw new Error(`Backup restore target must be a directory: ${targetPath}`);
		if ((await fs$1.readdir(targetPath)).length > 0) throw new Error(`Backup restore target directory must be empty: ${targetPath}`);
		return { created: false };
	} catch (error) {
		if (error.code !== "ENOENT") throw error;
	}
	await fs$1.mkdir(targetPath, {
		recursive: true,
		mode: 448
	});
	return { created: true };
}
async function cleanupFailedRestore(targetPath, created) {
	if (created) {
		await fs$1.rm(targetPath, {
			recursive: true,
			force: true
		});
		return;
	}
	for (const entry of await fs$1.readdir(targetPath)) await fs$1.rm(path.join(targetPath, entry), {
		recursive: true,
		force: true
	});
}
async function extractBackupArchive(archivePath, targetPath, hardlinkTargets) {
	let extractionError;
	await tar.x({
		file: archivePath,
		gzip: true,
		maxDecompressionRatio: BACKUP_MAX_DECOMPRESSION_RATIO,
		cwd: targetPath,
		strict: false,
		preserveOwner: false,
		onReadEntry: (entry) => {
			const target = hardlinkTargets.get(entry.path);
			if (target !== void 0) entry.linkpath = target;
		},
		onwarn: (code, message, data) => {
			extractionError ??= data instanceof Error ? data : Object.assign(/* @__PURE__ */ new Error(`${code}: ${message}`), data);
		}
	});
	if (extractionError) throw extractionError;
}
function formatRestoreResult(result) {
	return [
		`Backup archive restored to staging: ${shortenHomePath(result.targetPath)}`,
		`Verified archive: ${shortenHomePath(result.archivePath)}`,
		`Archive root: ${result.archiveRoot}`,
		`Archive entries restored: ${result.entryCount}`,
		"",
		"Rollback warnings:",
		...result.warnings.map((warning) => `- ${warning}`),
		"",
		"Activation is explicit: stop the Gateway, move the restored asset tree into place or point OPENCLAW_STATE_DIR at the restored state asset, then run `openclaw doctor`."
	].join("\n");
}
/** Verify first, then extract a whole backup archive into a fresh staging directory. */
async function backupRestoreCommand(runtime, options) {
	const targetPath = resolveRequiredBackupPath(options.target, "--target");
	await assertTargetOutsideLiveState(targetPath);
	const { result: verified, hardlinkTargets } = await prepareBackupArchive(options.archive);
	const target = await prepareRestoreTarget(targetPath);
	try {
		await extractBackupArchive(verified.archivePath, targetPath, hardlinkTargets);
	} catch (extractionError) {
		try {
			await cleanupFailedRestore(targetPath, target.created);
		} catch (cleanupError) {
			throw new AggregateError([extractionError, cleanupError], `Backup restore failed and the incomplete target could not be cleaned: ${targetPath}. Cleanup error: ${formatErrorMessage(cleanupError)}`, { cause: extractionError });
		}
		throw new Error(`Backup restore failed; the incomplete target was cleaned: ${targetPath}`, { cause: extractionError });
	}
	const result = {
		...verified,
		targetPath,
		warnings: [...BACKUP_RESTORE_WARNINGS]
	};
	if (options.json) writeRuntimeJson(runtime, result);
	else runtime.log(formatRestoreResult(result));
	return result;
}
//#endregion
//#region src/commands/backup-schedule.ts
const BACKUP_CRON_JOB_NAME = "openclaw-backup-scheduled";
const LOCAL_GATEWAY_REQUIRED_ERROR = "backup enable manages backups on the Gateway host and currently requires a local Gateway. Create the cron job manually with openclaw cron add for remote Gateways.";
/**
* Unattended pushed schedules make credential retention durable in remote
* history, so they redact by default; --include-secrets is the explicit
* full-fidelity override. Local (non-push) schedules keep full fidelity for
* complete restores.
*/
function resolveScheduledRedaction(options) {
	if (options.excludeSecrets && options.includeSecrets) throw new Error("Use either --exclude-secrets or --include-secrets, not both.");
	if (!options.push) return options.excludeSecrets === true;
	return options.includeSecrets !== true;
}
function buildScheduledArgv(options, repositoryPath, redactSecrets) {
	const agent = options.agent?.trim();
	if (options.agent !== void 0 && !agent) throw new Error("--agent must not be blank");
	if (options.globalOnly && agent) throw new Error("Use either --global-only or --agent <id>, not both.");
	const agentId = agent ? resolveConfiguredAgentId(getRuntimeConfig({ skipPluginValidation: true }), normalizeAgentId(agent)) : void 0;
	return [
		"openclaw",
		"backup",
		"git",
		"create",
		"--repository",
		repositoryPath,
		...options.globalOnly ? ["--global"] : agentId ? ["--agent", agentId] : ["--all"],
		...options.push ? ["--push"] : [],
		...redactSecrets ? ["--exclude-secrets"] : []
	];
}
async function assertLocalGatewayScheduleTarget(options) {
	if (!await isImplicitLocalGatewayTargetFromCli(options)) throw new Error(LOCAL_GATEWAY_REQUIRED_ERROR);
}
async function backupEnableCommand(runtime, options) {
	await assertLocalGatewayScheduleTarget(options);
	const repositoryPath = resolveRequiredBackupPath(options.repository, "--repository");
	const every = options.every?.trim() ?? "24h";
	const everyMs = parseDurationMs(every, { defaultUnit: "ms" });
	if (!Number.isSafeInteger(everyMs) || everyMs <= 0) throw new Error("--every must be a positive duration such as 6h or 24h.");
	const redactSecrets = resolveScheduledRedaction(options);
	const spec = {
		declarationKey: BACKUP_CRON_JOB_NAME,
		name: BACKUP_CRON_JOB_NAME,
		enabled: true,
		schedule: {
			kind: "every",
			everyMs
		},
		sessionTarget: "isolated",
		wakeMode: "now",
		payload: {
			kind: "command",
			argv: buildScheduledArgv(options, repositoryPath, redactSecrets)
		},
		delivery: { mode: "none" }
	};
	if (options.push) {
		if ((await executeGitCommand(repositoryPath, [
			"remote",
			"get-url",
			"origin"
		])).code !== 0) throw new Error(`--push requires an origin remote. Run: openclaw backup git init --repository ${shortenHomePath(repositoryPath)} --remote <url>`);
		if (!redactSecrets) runtime.error(GIT_BACKUP_PUSH_CREDENTIAL_WARNING);
	}
	const result = await callGatewayFromCli("cron.add", options, spec);
	const id = result.job?.id;
	if (!id) throw new Error("cron.add returned no scheduled backup job id.");
	const updated = result.created === false;
	runtime.log(`Scheduled Git backups ${updated ? "updated" : "enabled"}: every ${every} to ${shortenHomePath(repositoryPath)}`);
	return {
		id,
		updated
	};
}
async function backupDisableCommand(runtime, options) {
	await assertLocalGatewayScheduleTarget(options);
	const { jobs } = await listCronJobsFromGateway(options, { includeDisabled: true });
	const existing = jobs.find((job) => job.declarationKey === BACKUP_CRON_JOB_NAME);
	if (!existing) {
		runtime.log("Scheduled Git backups are already disabled.");
		return { removed: false };
	}
	await callGatewayFromCli("cron.remove", options, { id: existing.id });
	runtime.log("Scheduled Git backups disabled.");
	return { removed: true };
}
//#endregion
//#region src/commands/backup-sqlite.ts
const OPENCLAW_SNAPSHOT_READ_OPTIONS = { allowedDatabaseRoles: ["global", "agent"] };
async function backupSqliteCreateCommand(runtime, options) {
	const repositoryPath = resolveRequiredBackupPath(options.repository, "--repository");
	try {
		const database = await resolveSnapshotDatabase(options);
		const result = await createLocalSqliteSnapshotProvider({ repositoryPath }).create(database);
		const report = {
			ok: true,
			snapshotPath: result.ref.path,
			manifest: result.manifest
		};
		recordBackupOutcomeBestEffort(runtime, {
			kind: "sqlite-snapshot",
			archivePath: report.snapshotPath,
			status: "ok"
		});
		writeCreateResult(runtime, options, report);
		return report;
	} catch (error) {
		recordBackupOutcomeBestEffort(runtime, {
			kind: "sqlite-snapshot",
			archivePath: repositoryPath,
			status: "failed",
			error: formatErrorMessage(error)
		});
		throw error;
	}
}
async function backupSqliteListCommand(runtime, options) {
	const repositoryPath = resolveRequiredBackupPath(options.repository, "--repository");
	const report = {
		ok: true,
		repositoryPath,
		snapshots: await createLocalSqliteSnapshotProvider({
			repositoryPath,
			...OPENCLAW_SNAPSHOT_READ_OPTIONS
		}).list()
	};
	writeListResult(runtime, options, report);
	return report;
}
async function backupSqliteVerifyCommand(runtime, snapshot, options) {
	const resolved = resolveSnapshot(snapshot, options.scratch);
	const verified = await resolved.provider.verify(resolved.ref);
	const report = {
		ok: true,
		snapshotPath: resolved.ref.path,
		manifest: verified.manifest
	};
	writeVerifyResult(runtime, options, report);
	return report;
}
async function backupSqliteRestoreCommand(runtime, snapshot, options) {
	const resolved = resolveSnapshot(snapshot);
	const targetPath = resolveRequiredBackupPath(options.target, "--target");
	const restored = await resolved.provider.restoreFresh(resolved.ref, targetPath);
	const report = {
		ok: true,
		snapshotPath: resolved.ref.path,
		targetPath,
		manifest: restored.manifest
	};
	writeRestoreResult(runtime, options, report);
	return report;
}
async function resolveSnapshotDatabase(options) {
	const rawAgentId = options.agent?.trim();
	if (options.agent !== void 0 && !rawAgentId) throw new Error("--agent must not be blank");
	if (options.global === true && rawAgentId) throw new Error("Choose exactly one SQLite snapshot source: --global or --agent <id>.");
	if (options.global !== true && !rawAgentId) throw new Error("Choose a SQLite snapshot source: --global or --agent <id>.");
	if (options.global === true) return {
		path: await fs$1.realpath(resolveOpenClawStateSqlitePath()),
		identity: { role: "global" }
	};
	const config = getRuntimeConfig({ skipPluginValidation: true });
	const agentId = resolveConfiguredAgentId(config, normalizeAgentId(rawAgentId));
	const agentRoot = await resolveBackupAgentRoot(config, agentId);
	return {
		path: await fs$1.realpath(agentRoot.databasePath),
		identity: {
			role: "agent",
			agentId
		}
	};
}
function resolveSnapshot(snapshot, scratch) {
	const snapshotPath = resolveRequiredBackupPath(snapshot, "<snapshot>");
	const repositoryPath = path.dirname(snapshotPath);
	return {
		provider: createLocalSqliteSnapshotProvider({
			repositoryPath,
			validationRootPath: scratch ? resolveRequiredBackupPath(scratch, "--scratch") : path.dirname(repositoryPath),
			...OPENCLAW_SNAPSHOT_READ_OPTIONS
		}),
		ref: { path: snapshotPath }
	};
}
function formatDatabaseIdentity(database) {
	if (database.role === "global") return "global";
	if (database.role === "agent") return `agent:${database.agentId}`;
	return database.id;
}
function writeCreateResult(runtime, options, report) {
	if (options.json) {
		writeRuntimeJson(runtime, report);
		return;
	}
	runtime.log([
		`SQLite snapshot created: ${shortenHomePath(report.snapshotPath)}`,
		`Database: ${formatDatabaseIdentity(report.manifest.database)}`,
		`Size: ${report.manifest.artifact.sizeBytes} bytes`
	].join("\n"));
}
function writeListResult(runtime, options, report) {
	if (options.json) {
		writeRuntimeJson(runtime, report);
		return;
	}
	if (report.snapshots.length === 0) {
		runtime.log(`No SQLite snapshots in ${shortenHomePath(report.repositoryPath)}.`);
		return;
	}
	runtime.log(report.snapshots.map((snapshot) => `${snapshot.manifest.createdAt}  ${formatDatabaseIdentity(snapshot.manifest.database)}  ${snapshot.manifest.artifact.sizeBytes} bytes  ${shortenHomePath(snapshot.ref.path)}`).join("\n"));
}
function writeVerifyResult(runtime, options, report) {
	if (options.json) {
		writeRuntimeJson(runtime, report);
		return;
	}
	runtime.log(`SQLite snapshot verified: ${shortenHomePath(report.snapshotPath)} (${formatDatabaseIdentity(report.manifest.database)})`);
}
function writeRestoreResult(runtime, options, report) {
	if (options.json) {
		writeRuntimeJson(runtime, report);
		return;
	}
	runtime.log(`SQLite snapshot restored: ${shortenHomePath(report.targetPath)} (${formatDatabaseIdentity(report.manifest.database)})`);
}
//#endregion
//#region src/cli/program/register.backup.ts
/** Register backup create/verify subcommands. */
function registerBackupCommand(program) {
	const backup = program.command("backup").description("Create, verify, and restore backup archives and SQLite snapshots").addHelpText("after", () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/backup", "docs.openclaw.ai/cli/backup")}\n`);
	backup.command("create").description("Write a backup archive for config, credentials, sessions, and workspaces").option("--output <path>", "Archive path or destination directory").option("--json", "Output JSON", false).option("--dry-run", "Print the backup plan without writing the archive", false).option("--verify", "Verify the archive after writing it", false).option("--only-config", "Back up only the active JSON config file", false).option("--no-include-workspace", "Exclude workspace directories from the backup").addHelpText("after", () => `\n${theme.heading("Examples:")}\n${formatHelpExamples([
		["openclaw backup create", "Create a timestamped backup in the current directory."],
		["openclaw backup create --output ~/Backups", "Write the archive into an existing backup directory."],
		["openclaw backup create --dry-run --json", "Preview the archive plan without writing any files."],
		["openclaw backup create --verify", "Create the archive and immediately validate its manifest and payload layout."],
		["openclaw backup create --no-include-workspace", "Back up state/config without agent workspace files."],
		["openclaw backup create --only-config", "Back up only the active JSON config file."]
	])}`).action(async (opts) => {
		await runCommandWithRuntime(defaultRuntime, async () => {
			await backupCreateCommand(defaultRuntime, {
				output: opts.output,
				json: Boolean(opts.json),
				dryRun: Boolean(opts.dryRun),
				verify: Boolean(opts.verify),
				onlyConfig: Boolean(opts.onlyConfig),
				includeWorkspace: opts.includeWorkspace
			});
		});
	});
	backup.command("verify <archive>").description("Validate a backup archive and its embedded manifest").option("--json", "Output JSON", false).addHelpText("after", () => `\n${theme.heading("Examples:")}\n${formatHelpExamples([["openclaw backup verify ./2026-03-09T08-00-00.000+08-00-openclaw-backup.tar.gz", "Check that the archive structure and manifest are intact."], ["openclaw backup verify ~/Backups/latest.tar.gz --json", "Emit machine-readable verification output."]])}`).action(async (archive, opts) => {
		await runCommandWithRuntime(defaultRuntime, async () => {
			await backupVerifyCommand(defaultRuntime, {
				archive,
				json: Boolean(opts.json)
			});
		});
	});
	backup.command("restore <archive>").description("Restore a verified backup archive to a fresh staging directory").requiredOption("--target <dir>", "Fresh target directory; non-empty directories are refused").option("--json", "Output JSON", false).addHelpText("after", () => `\n${theme.heading("Examples:")}\n${formatHelpExamples([["openclaw backup restore ~/Backups/latest.tar.gz --target ./restored-openclaw", "Verify, then extract the whole archive into a fresh staging directory."], ["openclaw backup restore ~/Backups/latest.tar.gz --target ./restored-openclaw --json", "Emit machine-readable restore details and rollback warnings."]])}`).action(async (archive, opts) => {
		await runCommandWithRuntime(defaultRuntime, async () => {
			await backupRestoreCommand(defaultRuntime, {
				archive,
				target: opts.target,
				json: Boolean(opts.json)
			});
		});
	});
	registerBackupSqliteCommands(backup);
	registerBackupGitCommands(backup);
	registerBackupScheduleCommands(backup);
}
function collectAgent(value, previous) {
	return [...previous, value];
}
function registerBackupScheduleCommands(backup) {
	addGatewayClientOptions(backup.command("enable").description("Provision a Gateway automation for scheduled Git backups").requiredOption("--repository <path>", "Git backup repository directory").option("--every <duration>", "Backup interval", "24h").option("--push", "Push the current branch to origin after each backup", false).option("--exclude-secrets", "Omit credential-bearing database tables", false).option("--include-secrets", "Keep credential-bearing tables in pushed scheduled backups", false).option("--global-only", "Back up only the shared state database", false).option("--agent <id>", "Back up only one agent database").action(async (opts) => {
		await runCommandWithRuntime(defaultRuntime, async () => {
			await backupEnableCommand(defaultRuntime, opts);
		});
	}));
	addGatewayClientOptions(backup.command("disable").description("Remove the scheduled Git backup automation").action(async (opts) => {
		await runCommandWithRuntime(defaultRuntime, async () => {
			await backupDisableCommand(defaultRuntime, opts);
		});
	}));
}
function registerBackupGitCommands(backup) {
	const git = backup.command("git").description("Create and restore deterministic versioned SQLite dumps in Git").action(() => {
		git.outputHelp();
		process.exitCode = 1;
	});
	git.command("init").description("Initialize or adopt an operator-owned Git backup repository").requiredOption("--repository <path>", "Git backup repository directory").option("--remote <url>", "Add the remote as origin").option("--json", "Output JSON", false).action(async (opts) => {
		await runCommandWithRuntime(defaultRuntime, async () => {
			await backupGitInitCommand(defaultRuntime, opts);
		});
	});
	git.command("create").description("Dump selected OpenClaw databases and commit one Git revision").requiredOption("--repository <path>", "Git backup repository directory").option("--all", "Back up the shared database and every registered agent database", false).option("--global", "Back up the shared OpenClaw state database", false).option("--agent <id>", "Back up an agent database (repeatable)", collectAgent, []).option("--push", "Push the current branch to origin", false).option("--exclude-secrets", "Omit credential-bearing database tables", false).option("--json", "Output JSON", false).action(async (opts) => {
		await runCommandWithRuntime(defaultRuntime, async () => {
			await backupGitCreateCommand(defaultRuntime, {
				repository: opts.repository,
				all: Boolean(opts.all),
				global: Boolean(opts.global),
				agents: opts.agent,
				push: Boolean(opts.push),
				excludeSecrets: Boolean(opts.excludeSecrets),
				json: Boolean(opts.json)
			});
		});
	});
	git.command("log").description("Show Git backup commits").requiredOption("--repository <path>", "Git backup repository directory").option("--limit <n>", "Maximum commits to show", (value) => parseStrictPositiveIntOption(value, "--limit"), 20).option("--json", "Output JSON", false).action(async (opts) => {
		await runCommandWithRuntime(defaultRuntime, async () => {
			await backupGitLogCommand(defaultRuntime, opts);
		});
	});
	git.command("verify").description("Restore and verify one database snapshot from a Git ref").requiredOption("--repository <path>", "Git backup repository directory").option("--ref <commit>", "Commit or ref to verify", "HEAD").option("--global", "Verify the shared state database", false).option("--agent <id>", "Verify one agent database").option("--json", "Output JSON", false).action(async (opts) => {
		await runCommandWithRuntime(defaultRuntime, async () => {
			await backupGitVerifyCommand(defaultRuntime, opts);
		});
	});
	git.command("restore").description("Restore one database snapshot from a Git ref to a fresh SQLite file").requiredOption("--repository <path>", "Git backup repository directory").requiredOption("--target <path>", "Fresh target path; existing files and sidecars are refused").option("--ref <commit>", "Commit or ref to restore", "HEAD").option("--global", "Restore the shared state database", false).option("--agent <id>", "Restore one agent database").option("--json", "Output JSON", false).action(async (opts) => {
		await runCommandWithRuntime(defaultRuntime, async () => {
			await backupGitRestoreCommand(defaultRuntime, opts);
		});
	});
}
function registerBackupSqliteCommands(backup) {
	const sqlite = backup.command("sqlite").description("Create, list, verify, and restore SQLite snapshots").action(() => {
		sqlite.outputHelp();
		process.exitCode = 1;
	});
	sqlite.command("create").description("Create a compact, verified snapshot of an OpenClaw SQLite database").option("--global", "Snapshot the shared OpenClaw state database", false).option("--agent <id>", "Snapshot one per-agent OpenClaw database").requiredOption("--repository <path>", "Snapshot repository directory").option("--json", "Output JSON", false).addHelpText("after", () => `\n${theme.heading("Examples:")}\n${formatHelpExamples([["openclaw backup sqlite create --global --repository ~/Backups/openclaw-sqlite", "Snapshot the shared state database."], ["openclaw backup sqlite create --agent main --repository ~/Backups/openclaw-sqlite", "Snapshot the main agent database."]])}`).action(async (opts) => {
		await runCommandWithRuntime(defaultRuntime, async () => {
			await backupSqliteCreateCommand(defaultRuntime, {
				global: Boolean(opts.global),
				agent: opts.agent,
				repository: opts.repository,
				json: Boolean(opts.json)
			});
		});
	});
	sqlite.command("list").description("List committed snapshots in a repository").requiredOption("--repository <path>", "Snapshot repository directory").option("--json", "Output JSON", false).action(async (opts) => {
		await runCommandWithRuntime(defaultRuntime, async () => {
			await backupSqliteListCommand(defaultRuntime, {
				repository: opts.repository,
				json: Boolean(opts.json)
			});
		});
	});
	sqlite.command("verify <snapshot>").description("Verify a snapshot manifest, artifact hash, SQLite integrity, and database owner").option("--scratch <path>", "Existing private directory for verification copies").option("--json", "Output JSON", false).action(async (snapshot, opts) => {
		await runCommandWithRuntime(defaultRuntime, async () => {
			await backupSqliteVerifyCommand(defaultRuntime, snapshot, {
				scratch: opts.scratch,
				json: Boolean(opts.json)
			});
		});
	});
	sqlite.command("restore <snapshot>").description("Restore a verified snapshot to a new SQLite database path").requiredOption("--target <path>", "Fresh target path; existing files and sidecars are refused").option("--json", "Output JSON", false).action(async (snapshot, opts) => {
		await runCommandWithRuntime(defaultRuntime, async () => {
			await backupSqliteRestoreCommand(defaultRuntime, snapshot, {
				target: opts.target,
				json: Boolean(opts.json)
			});
		});
	});
}
//#endregion
export { registerBackupCommand };