openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
678 lines (677 loc) • 33.9 kB
JavaScript
import { t as assertExperimentalClawsEnabled } from "./experimental-BMzbGmT5.mjs";
import { a as writeRuntimeJson, r as defaultRuntime } from "./runtime-BRg2NubB.mjs";
import { w as root } from "./fs-safe-BBzelG8F.mjs";
import { t as resolveIdentityPathViaExistingAncestorSync } from "./boundary-path-CAv_plX7.mjs";
import { r as resolveStateDir } from "./state-dir-CYkBUd_G.mjs";
import "./paths-BP0DYpE8.mjs";
import { i as tableExists } from "./openclaw-state-db-schema-helpers-DKz5xGgh.mjs";
import { n as listAgentEntries } from "./agent-roster-DH6r_8f4.mjs";
import { d as resolveAgentWorkspaceDir } from "./agent-scope-config-BEk6-ThS.mjs";
import { n as openExistingOpenClawStateDatabaseReadOnly } from "./openclaw-state-db-DJZ7EKoI.mjs";
import { r as withConfigSourceLocks } from "./write-lock-DKbjzOgd.mjs";
import { n as isAvatarDataUrl } from "./avatar-policy-DiIdI6oM.mjs";
import { c as readConfigFileSnapshot } from "./io.runtime-B134MOMz.mjs";
import "./config-VnHbY_QV.mjs";
import { t as CLAW_BOOTSTRAP_FILE_NAMES } from "./manifest-contract-DWbLdUlq.mjs";
import { n as readClawInstallRecordFromDatabase } from "./provenance-read.kernel-BQe6L4vF.mjs";
import { o as withAgentDeletion } from "./agent-lifecycle-registry-CxGB4gfC.mjs";
import { n as digestClawValue, t as digestClawBytes } from "./digest-CI8Suvd7.mjs";
import { a as validateAgentConfigKeys, i as resolveMigrationAgentSettings, n as inspectValueForSecret, o as withAuthoredAgentRoster, r as normalizeWorkspaceConfig, s as ClawMigrationError, t as containsPotentialSecret } from "./migrate-validation-B0aBx41B.mjs";
import { a as isPortableClawAvatar } from "./schema-portability-CE93_2TU.mjs";
import { n as readClawManifestFile } from "./reader-B1zrAPP0.mjs";
import { n as MAX_MANAGED_FILE_BYTES, r as MAX_MANAGED_WORKSPACE_BYTES } from "./source-limits-BA6q70Ye.mjs";
import { h as CLAW_OUTPUT_STABILITY } from "./path-containment-BxaeTcXi.mjs";
import { D as persistClawMigrationOwnership, S as readClawInstallRecords, h as readClawWorkspaceFiles, k as readClawSecondaryReferenceTables, l as CLAW_WORKSPACE_FILE_RECORD_SCHEMA_VERSION, p as readAllClawWorkspaceFiles } from "./package-remove-plan-CgRv4IRz.mjs";
import { i as logClawExperimentalWarning, o as buildClawAddPlan, t as emitClawFailure } from "./claws-cli-output-D7lm0ZSh.mjs";
import { a as portableOpenClawProfile, i as portableAgent } from "./export-CdaG59-k.mjs";
import { dirname, relative, resolve, sep } from "node:path";
import { lstat, mkdir, mkdtemp, realpath, rm, rmdir, unlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { createHash } from "node:crypto";
import { assertNoSymlinkParents } from "@openclaw/fs-safe/advanced";
import { stringify } from "yaml";
//#region src/claws/migrate-package.ts
function lstatMigrationPathIfExists(path) {
return lstat(path).catch((error) => {
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return;
throw error;
});
}
function packageIdentityDigest(files) {
const hash = createHash("sha256");
let byteLength = 0;
for (const [path, content] of [...files.entries()].toSorted(([left], [right]) => left.localeCompare(right))) {
const pathBytes = Buffer.from(path, "utf8");
hash.update(`${pathBytes.byteLength}:${pathBytes.toString("utf8")}:${content.byteLength}:`);
hash.update(content);
byteLength += content.byteLength;
}
return {
integrity: `sha256:${hash.digest("hex")}`,
byteLength
};
}
function generatedPackage(agentId, params) {
const bootstrapFiles = {};
const packageFiles = /* @__PURE__ */ new Map();
let body;
for (const file of params.files) {
if (file.name === "SOUL.md" && file.content.toString("utf8").trim().length > 0) {
body = file.content;
continue;
}
bootstrapFiles[file.name] = { source: `workspace/${file.name}` };
packageFiles.set(`workspace/${file.name}`, file.content);
}
const agent = portableAgent(params.agent, params.avatar);
const profile = portableOpenClawProfile(params.agent, []);
const manifest = {
schemaVersion: 1,
agent,
workspace: {
bootstrapFiles,
files: []
},
packages: [],
mcpServers: {},
cronJobs: []
};
const clawMarkdownFrontmatter = [
"---",
stringify(manifest).trimEnd(),
"---",
""
].join("\n");
packageFiles.set("CLAW.md", body ? Buffer.concat([Buffer.from(clawMarkdownFrontmatter, "utf8"), body]) : Buffer.from(`${clawMarkdownFrontmatter}\n`, "utf8"));
packageFiles.set("package.json", Buffer.from(`${JSON.stringify({
name: `openclaw-agent-${agentId}-local`,
version: "1.0.0",
type: "module",
openclaw: { claw: "CLAW.md" }
}, null, 2)}\n`, "utf8"));
if (profile) packageFiles.set("profiles/openclaw.yml", Buffer.from(stringify(profile), "utf8"));
return {
manifest,
profile,
packageFiles
};
}
async function createGeneratedPackage(root, packageFiles) {
const parent = dirname(root);
let existingAncestor = parent;
while (!await lstatMigrationPathIfExists(existingAncestor)) {
const next = dirname(existingAncestor);
if (next === existingAncestor) throw new ClawMigrationError("package_parent_unavailable", `Could not find an existing parent for generated package ${JSON.stringify(root)}.`, "$.packageRoot");
existingAncestor = next;
}
const ancestorInfo = await lstat(existingAncestor);
if (!ancestorInfo.isDirectory() || ancestorInfo.isSymbolicLink()) throw new ClawMigrationError("package_parent_unsafe", `Generated package parent ${JSON.stringify(existingAncestor)} must be a real directory.`, "$.packageRoot");
const realAncestor = await realpath(existingAncestor);
await assertNoSymlinkParents({
rootDir: realAncestor,
targetPath: parent,
allowMissing: true,
messagePrefix: "Generated Claw package parent"
});
await mkdir(parent, { recursive: true });
const parentInfo = await lstat(parent);
const realParent = await realpath(parent);
if (!parentInfo.isDirectory() || parentInfo.isSymbolicLink() || realParent !== parent) throw new ClawMigrationError("package_parent_unsafe", `Generated package parent ${JSON.stringify(parent)} changed or resolves through a symlink; inspect the state directory before retrying.`, "$.packageRoot");
await assertNoSymlinkParents({
rootDir: realParent,
targetPath: root,
allowMissing: true,
messagePrefix: "Generated Claw package"
});
await mkdir(root, { recursive: false });
try {
for (const [path, content] of packageFiles) {
const target = resolve(root, path);
const child = relative(root, target);
if (child === ".." || child.startsWith(`..${sep}`)) throw new Error("Generated package path escaped its destination.");
await mkdir(dirname(target), { recursive: true });
await writeFile(target, content, { flag: "wx" });
}
} catch (error) {
await removeGeneratedPackageIfUnchanged(root, packageFiles);
throw error;
}
}
async function removeGeneratedPackageIfUnchanged(root$2, packageFiles) {
for (const [path, expected] of [...packageFiles.entries()].toReversed()) {
const target = resolve(root$2, path);
const info = await lstatMigrationPathIfExists(target);
if (!info || !info.isFile() || info.isSymbolicLink() || info.nlink !== 1) continue;
const actual = await root(dirname(target)).then((parent) => parent.read(target.slice(dirname(target).length + 1), {
hardlinks: "reject",
maxBytes: MAX_MANAGED_FILE_BYTES,
symlinks: "reject"
})).catch(() => void 0);
if (actual && digestClawBytes(actual.buffer) === digestClawBytes(expected)) await unlink(target).catch(() => void 0);
}
for (const directory of [
resolve(root$2, "profiles"),
resolve(root$2, "workspace"),
root$2
]) await rmdir(directory).catch(() => void 0);
}
async function createPackagePreview(packageFiles) {
const root = await mkdtemp(resolve(tmpdir(), "openclaw-claws-migrate-"));
try {
for (const [path, content] of packageFiles) {
const target = resolve(root, path);
await mkdir(dirname(target), { recursive: true });
await writeFile(target, content, { flag: "wx" });
}
return root;
} catch (error) {
await rm(root, {
recursive: true,
force: true
});
throw error;
}
}
//#endregion
//#region src/claws/migrate-safety.ts
function clawMigrationPathsOverlap(left, right) {
const a = resolve(left);
const b = resolve(right);
return a === b || a.startsWith(`${b}${sep}`) || b.startsWith(`${a}${sep}`);
}
function assertPackageDestinationOutsideWorkspaces(params) {
if (clawMigrationPathsOverlap(params.packageRoot, params.workspace)) throw new ClawMigrationError("package_destination_overlaps_workspace", `Generated package destination ${JSON.stringify(params.packageRoot)} overlaps the existing workspace ${JSON.stringify(params.workspace)}. Choose a workspace outside the local Claw package directory before migrating.`, "$.packageRoot");
for (const other of params.configuredAgents) {
if (other.id === params.agentId) continue;
const otherWorkspace = resolveIdentityPathViaExistingAncestorSync(resolveAgentWorkspaceDir(params.config, other.id, params.env));
if (clawMigrationPathsOverlap(params.packageRoot, otherWorkspace)) throw new ClawMigrationError("package_destination_owned_by_agent", `Generated package destination ${JSON.stringify(params.packageRoot)} overlaps agent ${JSON.stringify(other.id)} workspace ${JSON.stringify(otherWorkspace)}. Move that workspace before migrating.`, "$.packageRoot");
}
const installOverlap = params.installs.find((record) => record.agentId !== params.agentId && clawMigrationPathsOverlap(params.packageRoot, resolveIdentityPathViaExistingAncestorSync(record.workspace)));
if (installOverlap) throw new ClawMigrationError("package_destination_owned_by_claw", `Generated package destination ${JSON.stringify(params.packageRoot)} overlaps Claw agent ${JSON.stringify(installOverlap.agentId)} workspace ${JSON.stringify(installOverlap.workspace)}. Move that workspace before migrating.`, "$.packageRoot");
}
async function assertWorkspaceSnapshotUnchanged(workspace, expectedFiles, readCurrentFiles) {
const currentFiles = await readCurrentFiles(workspace);
const expected = new Map(expectedFiles.map((file) => [file.path, file.contentDigest]));
const changed = currentFiles.find((file) => expected.get(file.name) !== file.digest);
const missing = expectedFiles.find((file) => !currentFiles.some((current) => current.name === file.path));
if (changed || missing || currentFiles.length !== expectedFiles.length) {
const name = changed?.name ?? missing?.path ?? "selected prompt files";
throw new ClawMigrationError("workspace_file_changed_after_consent", `Existing workspace file ${JSON.stringify(name)} changed after consent. Rerun migrate to review the current workspace.`, `$.workspace.${name}`);
}
}
//#endregion
//#region src/claws/migrate-workspace-files.ts
async function readSelectedWorkspaceFiles(workspace) {
const root$1 = await root(workspace, {
hardlinks: "reject",
maxBytes: MAX_MANAGED_FILE_BYTES,
symlinks: "reject"
});
const captured = [];
let totalBytes = 0;
for (const name of CLAW_BOOTSTRAP_FILE_NAMES) {
const info = await lstatMigrationPathIfExists(resolve(workspace, name));
if (!info) continue;
if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1) throw new ClawMigrationError("workspace_file_unsafe", `${name} must be a regular, non-symlinked, non-hardlinked file.`, `$.workspace.${name}`);
const read = await root$1.read(name, {
hardlinks: "reject",
maxBytes: MAX_MANAGED_FILE_BYTES,
symlinks: "reject"
});
let text;
try {
text = new TextDecoder("utf-8", { fatal: true }).decode(read.buffer);
} catch {
throw new ClawMigrationError("workspace_file_encoding_unsupported", `${name} is not valid UTF-8 text and cannot be represented as an agent prompt file.`, `$.workspace.${name}`);
}
if (containsPotentialSecret(text)) throw new ClawMigrationError("workspace_file_secret_detected", `Potential secret material was found in ${name}. Remove it or leave this file unmanaged; the matching text was not displayed.`, `$.workspace.${name}`);
totalBytes += read.buffer.byteLength;
if (totalBytes > 4194304) throw new ClawMigrationError("workspace_files_too_large", `Selected prompt files exceed the ${MAX_MANAGED_WORKSPACE_BYTES}-byte Claw workspace limit.`, "$.workspace");
captured.push({
name,
content: read.buffer,
digest: digestClawBytes(read.buffer)
});
}
return captured;
}
//#endregion
//#region src/claws/migrate.ts
const CLAW_MIGRATION_PLAN_SCHEMA_VERSION = "openclaw.clawMigrationPlan.v1";
const CLAW_MIGRATION_RESULT_SCHEMA_VERSION = "openclaw.clawMigrationResult.v1";
const AGENT_ID_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
const MIGRATION_RETAINED_PATHS = [
"BOOTSTRAP.md (one-time workspace seed)",
"credentials and auth state",
"session indexes and transcripts",
"agent databases and runtime state",
"all other workspace files and directories"
];
async function readOwnership(options, agentId) {
const database = await openExistingOpenClawStateDatabaseReadOnly(options);
if (!database) return {
install: void 0,
workspaceFiles: [],
installs: [],
allWorkspaceFiles: [],
secondaryReferences: []
};
try {
const db = database.db;
const readOptions = {
...options,
database,
readOnly: true
};
const hasInstallTable = tableExists(db, "claw_installs");
const hasWorkspaceFileTable = tableExists(db, "claw_workspace_files");
return {
install: hasInstallTable ? readClawInstallRecordFromDatabase(db, agentId) : void 0,
workspaceFiles: hasWorkspaceFileTable ? readClawWorkspaceFiles(agentId, readOptions) : [],
installs: hasInstallTable ? readClawInstallRecords(readOptions) : [],
allWorkspaceFiles: hasWorkspaceFileTable ? readAllClawWorkspaceFiles(readOptions) : [],
secondaryReferences: readClawSecondaryReferenceTables(db, agentId)
};
} finally {
database.walMaintenance.close();
}
}
async function canonicalExistingWorkspace(path) {
const absolute = resolve(path);
const stat = await lstat(absolute).catch(() => void 0);
if (!stat || !stat.isDirectory() || stat.isSymbolicLink()) throw new ClawMigrationError("workspace_unavailable", `Workspace ${JSON.stringify(absolute)} must already exist as a local directory and cannot be a symlink.`, "$.workspace");
return await realpath(absolute);
}
function sanitizeAgentPreview(agent) {
const avatar = agent.identity?.avatar;
if (!avatar?.startsWith("data:image/")) return agent;
return {
...agent,
identity: {
...agent.identity,
avatar: `image data URL (${Buffer.byteLength(avatar, "utf8")} bytes; ${digestClawBytes(Buffer.from(avatar))})`
}
};
}
function summarizePackageFiles(packageFiles) {
return [...packageFiles.entries()].map(([path, content]) => ({
path,
byteLength: content.byteLength,
digest: digestClawBytes(content)
})).toSorted((left, right) => left.path.localeCompare(right.path));
}
function buildPlanIntegrity(addPlan, packageFiles) {
return digestClawValue({
schemaVersion: CLAW_MIGRATION_PLAN_SCHEMA_VERSION,
addPlan,
packageFiles: summarizePackageFiles(packageFiles),
retained: MIGRATION_RETAINED_PATHS
});
}
function adoptMigrationActions(plan) {
plan.actions = plan.actions.map((action) => {
if (action.kind !== "workspaceFile" && action.kind !== "agent" && action.kind !== "workspace") return action;
return {
...action,
action: "reuse",
details: {
...action.details,
expectedState: action.kind === "workspaceFile" ? "present-matching" : "present"
}
};
});
}
function buildMigrationAddPlan(read, packageFiles, context) {
const otherAgents = context.configuredAgents.filter((entry) => entry.id !== context.agentId);
return buildClawAddPlan({
manifest: read.manifest,
clawMarkdownBody: read.clawMarkdownBody,
openClawProfile: read.openClawProfile,
source: {
...read.source,
...packageIdentityDigest(packageFiles)
},
context: {
config: context.config,
agentId: context.agentId,
workspace: context.workspace,
existingAgentIds: otherAgents.map((entry) => entry.id),
existingWorkspacePaths: otherAgents.map((entry) => resolveAgentWorkspaceDir(context.config, entry.id, context.env)),
resumableWorkspace: context.workspace,
sourceReferenceRoot: context.packageRoot
}
});
}
async function buildClawMigrationPlan(params) {
const options = params.options ?? {};
const agentId = params.agentId;
if (!AGENT_ID_PATTERN.test(agentId)) throw new ClawMigrationError("invalid_agent_id", `Agent id ${JSON.stringify(agentId)} is not a valid Claw agent id.`, "$.agent.id");
const agents = listAgentEntries(params.config).filter((entry) => entry.id === agentId);
if (agents.length !== 1) throw new ClawMigrationError(agents.length === 0 ? "agent_not_found" : "agent_ambiguous", agents.length === 0 ? `No configured local agent matches ${JSON.stringify(agentId)}.` : `More than one configured agent resolves to ${JSON.stringify(agentId)}; resolve the duplicate ownership before migrating.`, "$.agent.id");
const agent = agents[0];
validateAgentConfigKeys(agent);
const migrationAgent = resolveMigrationAgentSettings(params.config, agent);
const ownership = await readOwnership(options, agentId);
if (ownership.install) throw new ClawMigrationError("agent_already_managed", `Agent ${JSON.stringify(agentId)} already has Claw ownership. Use claws status/update/remove instead of migrating it again.`);
if (ownership.secondaryReferences.length > 0) throw new ClawMigrationError("secondary_resources_unclaimed", `Agent ${JSON.stringify(agentId)} has unclaimed Claw resource references in ${ownership.secondaryReferences.join(", ")}. Reconcile those resources before migrating.`, "$.agent.id");
if (ownership.workspaceFiles.length > 0) throw new ClawMigrationError("workspace_ownership_unclaimed", `Agent ${JSON.stringify(agentId)} has Claw workspace-file ownership records without an install record. Reconcile those records before migrating.`, "$.workspace");
const workspace = await canonicalExistingWorkspace(resolveAgentWorkspaceDir(params.config, agentId, options.env));
const packageRoot = resolveIdentityPathViaExistingAncestorSync(resolve(resolveStateDir(options.env), "claws", "local", agentId));
if (await lstatMigrationPathIfExists(packageRoot)) throw new ClawMigrationError("package_destination_exists", `Local Claw package destination ${JSON.stringify(packageRoot)} already exists. Move or inspect it before migrating.`, "$.packageRoot");
const configuredAgents = listAgentEntries(params.config);
assertPackageDestinationOutsideWorkspaces({
agentId,
packageRoot,
workspace,
configuredAgents,
installs: ownership.installs,
config: params.config,
env: options.env
});
for (const other of configuredAgents) {
if (other.id === agentId) continue;
const otherWorkspace = resolveIdentityPathViaExistingAncestorSync(resolveAgentWorkspaceDir(params.config, other.id, options.env));
if (clawMigrationPathsOverlap(workspace, otherWorkspace)) throw new ClawMigrationError("workspace_ownership_ambiguous", `Workspace ${JSON.stringify(workspace)} overlaps agent ${JSON.stringify(other.id)} at ${JSON.stringify(otherWorkspace)}. Resolve workspace ownership before migrating.`, "$.workspace");
}
const installOverlap = ownership.installs.find((record) => record.agentId !== agentId && clawMigrationPathsOverlap(workspace, resolveIdentityPathViaExistingAncestorSync(record.workspace)));
if (installOverlap) throw new ClawMigrationError("workspace_owned_by_claw", `Workspace ${JSON.stringify(workspace)} is already tracked by Claw agent ${JSON.stringify(installOverlap.agentId)}.`, "$.workspace");
const selectedFiles = await readSelectedWorkspaceFiles(workspace);
const selectedPaths = new Set(selectedFiles.map((file) => file.name));
const conflictingFile = ownership.allWorkspaceFiles.find((file) => file.agentId !== agentId && resolveIdentityPathViaExistingAncestorSync(file.workspace) === workspace && selectedPaths.has(file.path));
if (conflictingFile) throw new ClawMigrationError("workspace_file_already_owned", `${conflictingFile.path} is already tracked by Claw agent ${JSON.stringify(conflictingFile.agentId)}.`, `$.workspace.${conflictingFile.path}`);
const avatar = agent.identity?.avatar?.trim();
const projected = generatedPackage(agentId, {
agent: migrationAgent,
avatar: avatar && isAvatarDataUrl(avatar) && isPortableClawAvatar(avatar) ? avatar : void 0,
files: selectedFiles
});
if (inspectValueForSecret({
agent: projected.manifest.agent,
profile: projected.profile
})) throw new ClawMigrationError("agent_setting_secret_detected", "Potential secret material was found in a Claw v1 agent setting. The matching value was not displayed; remove it or keep the setting unmanaged.", "$.agent");
const packagePreview = await createPackagePreview(projected.packageFiles);
try {
const loaded = await readClawManifestFile(packagePreview);
if (!loaded.ok) throw new ClawMigrationError("generated_package_invalid", loaded.diagnostics.map((diagnostic) => diagnostic.message).join("; "), loaded.diagnostics[0]?.path);
const addPlan = await buildMigrationAddPlan(loaded, projected.packageFiles, {
config: params.config,
agentId,
workspace,
packageRoot,
configuredAgents,
env: options.env
});
if (addPlan.blockers.length > 0) {
const first = addPlan.blockers[0];
throw new ClawMigrationError(first.code, first.message, first.path);
}
const comparablePlanAgent = normalizeWorkspaceConfig(migrationAgent, workspace);
if (digestClawValue(comparablePlanAgent) !== digestClawValue(addPlan.agent.config)) throw new ClawMigrationError("agent_settings_not_faithful", "The generated Claw v1 manifest would not reproduce the configured agent settings exactly. Review unsupported fields and defaults before migrating.", "$.agent");
for (const fileAction of addPlan.actions.filter((action) => action.kind === "workspaceFile")) {
const name = fileAction.id;
const captured = selectedFiles.find((file) => file.name === name);
if (!captured || fileAction.digest !== captured.digest) throw new ClawMigrationError("workspace_file_projection_changed", `The Claw package would not preserve the exact bytes of ${JSON.stringify(name)}.`, `$.workspace.${name}`);
}
adoptMigrationActions(addPlan);
const planIntegrity = buildPlanIntegrity(addPlan, projected.packageFiles);
addPlan.planIntegrity = planIntegrity;
const plan = {
schemaVersion: CLAW_MIGRATION_PLAN_SCHEMA_VERSION,
stability: "experimental",
dryRun: true,
mutationAllowed: false,
agentId,
workspace,
packageRoot,
packageName: loaded.source.name,
agent: projected.manifest.agent,
...projected.profile ? { openClawProfile: projected.profile } : {},
generatedPackageFiles: summarizePackageFiles(projected.packageFiles),
workspaceFiles: selectedFiles.map(({ name, content, digest }) => ({
path: resolve(workspace, name),
byteLength: content.byteLength,
digest
})),
retained: [...MIGRATION_RETAINED_PATHS],
planIntegrity,
blockers: []
};
const ownershipFiles = addPlan.actions.filter((action) => action.kind === "workspaceFile").map((action) => {
const sourcePath = action.source ? relative(plan.packageRoot, action.source).replaceAll(sep, "/") : "";
const path = relative(workspace, action.target).replaceAll(sep, "/");
if (!sourcePath || path.startsWith("../") || sourcePath.startsWith("../")) throw new ClawMigrationError("migration_path_invalid", `Could not bind ${JSON.stringify(action.id)} to the generated local package.`, `$.workspace.${action.id}`);
return {
schemaVersion: CLAW_WORKSPACE_FILE_RECORD_SCHEMA_VERSION,
agentId,
workspace,
path,
sourcePath,
contentDigest: action.digest,
status: "complete",
createdAtMs: 0,
updatedAtMs: 0
};
});
return {
plan: {
...plan,
agent: sanitizeAgentPreview(plan.agent)
},
addPlan,
manifest: loaded.manifest,
...loaded.openClawProfile ? { profile: loaded.openClawProfile } : {},
packageFiles: projected.packageFiles,
ownershipFiles
};
} finally {
await rm(packagePreview, {
recursive: true,
force: true
});
}
}
async function applyClawMigrationPlan(params) {
const options = params.options ?? {};
const root = params.migration.plan.packageRoot;
const currentWorkspace = await canonicalExistingWorkspace(resolveAgentWorkspaceDir(params.config, params.migration.plan.agentId, options.env));
if (currentWorkspace !== params.migration.plan.workspace) throw new ClawMigrationError("migration_changed", "The configured workspace changed after consent. Rerun migrate and review the new plan.");
const currentOwnership = await readOwnership(options, params.migration.plan.agentId);
if (currentOwnership.secondaryReferences.length > 0) throw new ClawMigrationError("secondary_resources_unclaimed", `Agent ${JSON.stringify(params.migration.plan.agentId)} now has unclaimed Claw resource references in ${currentOwnership.secondaryReferences.join(", ")}; rerun migrate after reconciling them.`, "$.agent.id");
assertPackageDestinationOutsideWorkspaces({
agentId: params.migration.plan.agentId,
packageRoot: root,
workspace: currentWorkspace,
configuredAgents: listAgentEntries(params.config),
installs: currentOwnership.installs,
config: params.config,
env: options.env
});
if (await lstatMigrationPathIfExists(root)) throw new ClawMigrationError("package_destination_exists", `Generated package destination ${JSON.stringify(root)} appeared after planning; rerun migrate to review a fresh plan.`, "$.packageRoot");
await params.assertCurrentConfig?.();
await createGeneratedPackage(root, params.migration.packageFiles);
try {
await assertWorkspaceSnapshotUnchanged(currentWorkspace, params.migration.ownershipFiles, readSelectedWorkspaceFiles);
const read = await readClawManifestFile(root);
if (!read.ok) throw new ClawMigrationError("generated_package_invalid", read.diagnostics.map((diagnostic) => diagnostic.message).join("; "));
const finalPlan = await buildMigrationAddPlan(read, params.migration.packageFiles, {
config: params.config,
agentId: params.migration.plan.agentId,
workspace: params.migration.plan.workspace,
packageRoot: root,
configuredAgents: listAgentEntries(params.config),
env: options.env
});
for (const fileAction of finalPlan.actions.filter((action) => action.kind === "workspaceFile")) {
const expected = params.migration.ownershipFiles.find((file) => file.path === fileAction.id);
if (!expected || fileAction.digest !== expected.contentDigest) throw new ClawMigrationError("workspace_file_projection_changed", `The existing ${JSON.stringify(fileAction.id)} changed after consent. Rerun migrate to review the current file.`, `$.workspace.${fileAction.id}`);
}
adoptMigrationActions(finalPlan);
const finalIntegrity = buildPlanIntegrity(finalPlan, params.migration.packageFiles);
if (finalIntegrity !== params.migration.plan.planIntegrity) throw new ClawMigrationError("migration_changed", "The agent, workspace files, or local ownership changed after consent. The generated package was removed; rerun migrate and review the new plan.");
finalPlan.claw = read.source;
finalPlan.planIntegrity = finalIntegrity;
await assertWorkspaceSnapshotUnchanged(currentWorkspace, params.migration.ownershipFiles, readSelectedWorkspaceFiles);
const finalOwnershipFiles = params.migration.ownershipFiles.map((file) => ({
...file,
createdAtMs: Date.now(),
updatedAtMs: Date.now()
}));
await params.assertCurrentConfig?.();
persistClawMigrationOwnership(finalPlan, finalOwnershipFiles, options);
return {
schemaVersion: CLAW_MIGRATION_RESULT_SCHEMA_VERSION,
stability: "experimental",
dryRun: false,
status: "complete",
agentId: params.migration.plan.agentId,
workspace: params.migration.plan.workspace,
packageRoot: root,
planIntegrity: finalIntegrity
};
} catch (error) {
await removeGeneratedPackageIfUnchanged(root, params.migration.packageFiles);
throw error;
}
}
//#endregion
//#region src/cli/claws-migrate-cli.runtime.ts
async function readMigrationConfig() {
const snapshot = await readConfigFileSnapshot({
observe: false,
isolateEnv: true
});
if (!snapshot.exists || !snapshot.valid) throw new ClawMigrationError("migration_config_unavailable", "Migration requires an existing valid local configuration. Repair the config before retrying.");
return {
config: withAuthoredAgentRoster(snapshot.runtimeConfig, snapshot.sourceConfigBeforeMigrations),
sources: [.../* @__PURE__ */ new Set([
snapshot.path,
await realpath(snapshot.path),
...snapshot.includedPaths ?? []
])].toSorted()
};
}
function logMigrationPlan(plan, runtime) {
logClawExperimentalWarning(runtime);
runtime.log(`Existing agent: ${plan.agentId}`);
runtime.log(`Workspace: ${plan.workspace}`);
runtime.log(`Local Claw package: ${plan.packageRoot}`);
runtime.log(`Portable identity: ${JSON.stringify(plan.agent)}`);
if (plan.openClawProfile) runtime.log(`OpenClaw profile: ${JSON.stringify(plan.openClawProfile.agent)}`);
runtime.log("Generated local Claw package files:");
for (const file of plan.generatedPackageFiles) runtime.log(` ${file.path} (${file.byteLength} bytes, ${file.digest})`);
if (plan.workspaceFiles.length === 0) runtime.log("Existing prompt files becoming Claw-managed: none");
else {
runtime.log("Existing prompt files becoming Claw-managed (contents will not be rewritten):");
for (const file of plan.workspaceFiles) runtime.log(` ${file.path} (${file.byteLength} bytes, ${file.digest})`);
}
runtime.log("Retained outside Claw ownership:");
for (const item of plan.retained) runtime.log(` ${item}`);
runtime.log(`Plan integrity: ${plan.planIntegrity}`);
}
function emitMigrationFailure(runtime, json, code, message, path = "$") {
emitClawFailure(runtime, json, message, {
schemaVersion: CLAW_MIGRATION_PLAN_SCHEMA_VERSION,
stability: CLAW_OUTPUT_STABILITY,
ok: false,
mutationAllowed: false,
error: {
code,
message
},
blockers: [{
code,
path,
message
}]
});
}
async function runClawsMigrateCommand(agentId, opts, runtime = defaultRuntime) {
assertExperimentalClawsEnabled();
if (!opts.dryRun && opts.yes && !opts.planIntegrity) {
emitMigrationFailure(runtime, opts.json, "plan_integrity_required", "Automated Claw migration requires --yes with --plan-integrity from the exact dry-run plan.");
return;
}
if (!opts.dryRun && !opts.yes && opts.json) {
emitMigrationFailure(runtime, true, "consent_required", "JSON migration requires --dry-run or --yes with --plan-integrity; interactive consent is available in human-readable mode.");
return;
}
let migration;
let previewConfig;
try {
previewConfig = await readMigrationConfig();
migration = await buildClawMigrationPlan({
agentId,
config: previewConfig.config,
options: { env: process.env }
});
} catch (error) {
const code = error instanceof ClawMigrationError ? error.code : "migration_plan_failed";
const message = error instanceof Error ? error.message : String(error);
const path = error instanceof ClawMigrationError ? error.path : "$";
emitMigrationFailure(runtime, opts.json, code, message, path);
return;
}
if (opts.dryRun) {
if (opts.json) writeRuntimeJson(runtime, migration.plan);
else logMigrationPlan(migration.plan, runtime);
return;
}
if (opts.yes && opts.planIntegrity !== migration.plan.planIntegrity) {
emitMigrationFailure(runtime, opts.json, "plan_integrity_mismatch", "Consent does not match the current migration plan. Run claws migrate with --dry-run and use its exact plan-integrity value.");
return;
}
if (!opts.yes) {
logMigrationPlan(migration.plan, runtime);
const { confirm, isCancel } = await import("@clack/prompts");
const confirmed = await confirm({
message: `Enroll existing agent ${JSON.stringify(agentId)} as a Claw?`,
initialValue: false
});
if (isCancel(confirmed) || !confirmed) {
runtime.log("Migration cancelled; no Claw ownership was recorded.");
return;
}
}
try {
const result = await withAgentDeletion(agentId, async () => await withConfigSourceLocks(previewConfig.sources, async (assertCurrent) => {
const changed = () => new ClawMigrationError("migration_changed", "The agent, workspace files, or ownership changed after consent. Review a fresh dry-run plan before retrying.");
const latest = await readMigrationConfig();
assertCurrent();
if (digestClawValue(latest.sources) !== digestClawValue(previewConfig.sources)) throw changed();
const current = await buildClawMigrationPlan({
agentId,
config: latest.config,
options: { env: process.env }
});
if (current.plan.planIntegrity !== migration.plan.planIntegrity) throw changed();
const expectedConfig = digestClawValue(latest);
return await applyClawMigrationPlan({
migration: current,
config: latest.config,
options: { env: process.env },
assertCurrentConfig: async () => {
assertCurrent();
const live = await readMigrationConfig();
assertCurrent();
if (digestClawValue(live) !== expectedConfig) throw changed();
}
});
}, process.env), { env: process.env });
if (opts.json) {
writeRuntimeJson(runtime, result);
return;
}
logClawExperimentalWarning(runtime);
runtime.log(`Migrated agent: ${result.agentId}`);
runtime.log(`Workspace: ${result.workspace}`);
runtime.log(`Local Claw package: ${result.packageRoot}`);
runtime.log(`Plan integrity: ${result.planIntegrity}`);
} catch (error) {
const code = error instanceof ClawMigrationError ? error.code : "migration_failed";
const message = error instanceof Error ? error.message : String(error);
const path = error instanceof ClawMigrationError ? error.path : "$";
emitMigrationFailure(runtime, opts.json, code, message, path);
}
}
//#endregion
export { runClawsMigrateCommand };