UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

875 lines (874 loc) • 39.7 kB
import { t as coerceErrorMessage } from "./error-coercion-CvJK4lmH.mjs"; import "./src-CdRlGRnW.mjs"; import { r as runSqliteDeferredTransactionSync } from "./sqlite-transaction-DsZ3X6nD.mjs"; import { n as normalizeAgentId } from "./agent-id-D1-Q6vR4.mjs"; import "./session-key-C09ow4h0.mjs"; import { n as listAgentEntries } from "./agent-roster-BXD1uMJD.mjs"; import { l as runOpenClawStateWriteTransaction, r as openOpenClawStateDatabase } from "./openclaw-state-db-D1jWDzK3.mjs"; import "./agent-scope-CG17uXFn.mjs"; import { r as normalizeConfiguredMcpServers } from "./mcp-config-normalize-DQmvZkAl.mjs"; import { r as getRuntimeConfig } from "./io.runtime-BsUXSsD4.mjs"; import "./config-DLycWFvB.mjs"; import { l as readAgentDeletionJournalInDatabase } from "./agent-deletion-journal-CDljXcnp.mjs"; import { t as MAX_WORKSPACE_BOOTSTRAP_FILE_BYTES } from "./workspace-bootstrap-read-B5B8MlNt.mjs"; import { c as withAgentDeletion } from "./agent-lifecycle-registry-kGv3atS9.mjs"; import { f as withAgentExecApprovalsRemoved } from "./exec-approvals-store-DGp2moY9.mjs"; import "./exec-approvals-B1rZTeax.mjs"; import { u as digestClawValue } from "./schema-portability-DPQGHUzw.mjs"; import { t as listConfiguredMcpServers } from "./mcp-config-tD8qD_3g.mjs"; import { a as withClawMcpLifecycleLease, n as unsetConfiguredMcpServer } from "./mcp-config-mutation-CHS7MdUl.mjs"; import { S as deleteClawCronRef, a as ClawRemoveError, b as clawCronGatewayJobMatchesRef, c as deletionEffects, f as readClawRemoveCronInventory, g as workspaceContainsUntrackedEntries, i as clawMonitorSnapshotSchema, m as removeClawWorkspaceFile, o as clawRemoveQuietRuntime, p as releaseClawRemoveRows, s as cleanupClawAgentFilesystem, w as markClawCronRefRemoved } from "./monitor-cleanup-contract-CjuEG_Vl.mjs"; import { a as prepareAgentDeleteDatabases, c as resolveSurvivingDatabaseFilePaths, i as isPathOwnedBySurvivingAgent, n as assertAgentSessionStoreDeletionSafe, s as readAgentDeleteDatabaseRegistry, t as AgentSharedStoreOwnerError } from "./agent-delete-databases-nBTK8a9n.mjs"; import { c as readClawMcpServerRefsByName, i as digestClawMcpServer, m as CLAW_OUTPUT_STABILITY, n as clawMcpRemovalSelector, o as planClawMcpServerRemoval, r as deleteClawMcpServerRef } from "./mcp-DOUItsfA.mjs"; import { C as readClawInstallRecord, E as releaseAdoptedClawInstallRecord, M as planClawPackageRemovals, O as updateClawInstallRecordStatus, a as orderClawPackageRemovals, c as readClawStatus, i as normalizeClawPackageCleanup, j as applyClawPackageRemovals, n as digestClawRemovalInstall, o as projectClawPackageRemovePlan, r as filterReferencedCleanup, t as digestClawPackageRemovalPlan } from "./package-remove-plan-ZBjC4Y9P.mjs"; import { t as readClawInstallRecordFromDatabase } from "./provenance-read.kernel-B_3aRsEz.mjs"; import { r as deleteAgentConfigEntry, t as AgentConfigPreconditionError } from "./agents-config-mutations-ge_DxA30.mjs"; import { isDeepStrictEqual } from "node:util"; //#region src/claws/lifecycle-remove-contract.ts const CLAW_REMOVE_PLAN_SCHEMA_VERSION = "openclaw.clawRemovePlan.v1"; const CLAW_REMOVE_RESULT_SCHEMA_VERSION = "openclaw.clawRemoveResult.v1"; //#endregion //#region src/claws/lifecycle-adopted-removal.ts function buildClawAdoptedRemovePlan(target, record, blockers) { const adoptedBlockers = [...blockers]; if (record.install.status !== "complete") adoptedBlockers.push({ code: "adopted_install_incomplete", message: `Adopted Claw ownership is ${record.install.status}; reconcile it before removal.` }); if (record.packages.length > 0 || record.mcpServers.length > 0 || record.cronJobs.length > 0) adoptedBlockers.push({ code: "adopted_managed_resources_present", message: "This adopted Claw has managed packages, MCP servers, or cron jobs. Removal will not touch the pre-existing agent; reconcile those Claw resources before releasing ownership." }); const actions = [ { kind: "agent", id: record.install.agentId, action: "retain", target: `agents.entries[${JSON.stringify(record.install.agentId)}]`, blocked: false, reason: "The agent existed before Claw migration and remains configured." }, { kind: "workspace", id: record.install.agentId, action: "retain", target: record.install.workspace, blocked: false, reason: "The workspace existed before Claw migration and remains in place." }, { kind: "agentState", id: record.install.agentId, action: "retain", target: "agent runtime state, credentials, and databases", blocked: false }, { kind: "sessionIndex", id: record.install.agentId, action: "retain", target: `session store entries for agent:${record.install.agentId}`, blocked: false }, { kind: "sessionTranscripts", id: record.install.agentId, action: "retain", target: "session transcripts", blocked: false }, ...record.workspaceFiles.map((file) => ({ kind: "workspaceFile", id: file.path, action: "retain", target: file.path, blocked: false, reason: "The file predated migration; only its Claw ownership record is released." })), { kind: "installRecord", id: record.install.agentId, action: "release", target: `claw_installs:${record.install.agentId}`, blocked: false, details: { expectedPlanIntegrity: record.install.planIntegrity } } ]; const planIdentity = { target, agentId: record.install.agentId, actions, blockers: adoptedBlockers }; return { schemaVersion: CLAW_REMOVE_PLAN_SCHEMA_VERSION, stability: CLAW_OUTPUT_STABILITY, dryRun: true, mutationAllowed: false, planIntegrity: digestClawValue(planIdentity), target, agentId: record.install.agentId, actions, blockers: adoptedBlockers }; } async function applyClawAdoptedRemovePlan(plan, options) { const agentId = plan.agentId; if (!agentId) throw new ClawRemoveError("remove_blocked", "The adopted Claw remove plan has no agent id."); return await withAgentDeletion(agentId, async () => { const record = (await readClawStatus(agentId, options)).records[0]; if (!record || record.install.agentOrigin !== "adopted" || record.install.status !== "complete" || record.packages.length > 0 || record.mcpServers.length > 0 || record.cronJobs.length > 0) throw new ClawRemoveError("remove_blocked", "Adopted Claw ownership now includes incomplete or managed secondary resources; review remove --dry-run and reconcile them first."); if (buildClawAdoptedRemovePlan(plan.target, record, []).planIntegrity !== plan.planIntegrity) throw new ClawRemoveError("remove_changed", "Claw-owned state changed while waiting to release adopted ownership; review a fresh remove --dry-run plan."); releaseAdoptedClawInstallRecord(agentId, record.install.planIntegrity, options); return { schemaVersion: CLAW_REMOVE_RESULT_SCHEMA_VERSION, stability: CLAW_OUTPUT_STABILITY, dryRun: false, status: "complete", agentId, agentRemoved: false, workspaceFiles: [], packages: [], mcpServers: [], cronJobs: [], packageRefsReleased: 0, warnings: ["Released Claw ownership. The pre-existing agent, workspace, credentials, databases, sessions, transcripts, and generated local package were retained."] }; }, options); } //#endregion //#region src/claws/lifecycle-bootstrap-removal.ts function clawBootstrapStateBlocksRemove(record) { return Boolean(record.install.bootstrap && (record.bootstrap.state === "unsafe" || record.bootstrap.state === "unknown")); } function planClawBootstrapRemoval(record) { if (!record.install.bootstrap) return; const blocked = clawBootstrapStateBlocksRemove(record); return { kind: "bootstrap", id: record.bootstrap.path, action: record.bootstrap.state === "pending" ? "delete" : "retain", target: `${record.bootstrap.workspace}:${record.bootstrap.path}`, blocked, details: { expectedState: record.bootstrap.state, contentDigest: record.install.bootstrap.contentDigest, sourcePath: record.install.bootstrap.sourcePath, lifecycle: "native-seed-once" }, ...record.bootstrap.state === "modified" ? { reason: "Local bootstrap content changed; preserve the file." } : record.bootstrap.state === "complete" ? { reason: "Native onboarding already consumed the bootstrap." } : {} }; } async function removeClawBootstrap(record, assertCurrent) { if (!record.install.bootstrap) return; if (record.bootstrap.state === "pending") return removeClawWorkspaceFile({ workspace: record.bootstrap.workspace, path: record.bootstrap.path, contentDigest: record.install.bootstrap.contentDigest, state: "unchanged" }, assertCurrent, MAX_WORKSPACE_BOOTSTRAP_FILE_BYTES); return record.bootstrap.state === "modified" ? { path: record.bootstrap.path, action: "retainedModified" } : { path: record.bootstrap.path, action: "missing" }; } //#endregion //#region src/claws/lifecycle-config-removal.ts function digestClawAgentRemovalSurface(config, agentId) { const normalizedId = normalizeAgentId(agentId); const surface = { bindings: (config.bindings ?? []).filter((binding) => normalizeAgentId(binding.agentId) === normalizedId), agentToAgentAllow: (config.tools?.agentToAgent?.allow ?? []).filter((entry) => entry === normalizedId) }; return digestClawValue(surface); } async function commitClawAgentConfigRemoval(params, assertCurrent) { const configBeforeDelete = params.config ?? getRuntimeConfig(); try { const committed = await deleteAgentConfigEntry({ agentId: params.agentId, assertCurrent, allowConfigSizeDrop: true, allowMissing: params.expectedState === "missing", fallbackWorkspace: params.fallbackWorkspace, validateConfig: (config) => { assertCurrent(); assertAgentSessionStoreDeletionSafe(config, params.agentId, params.stateDatabase); if (digestClawAgentRemovalSurface(config, params.agentId) !== params.expectedRemovalSurfaceDigest) throw params.onModified(); }, validate: (agent) => { if (params.expectedState === "missing") throw params.onModified(); if (digestClawValue(agent) !== params.expectedDigest) throw params.onModified(); } }); const fallbackEffects = deletionEffects(configBeforeDelete, params.agentId, params.fallbackWorkspace, params.stateDatabase?.env); return { agentRemoved: Boolean(committed.result), cleanupTargets: committed.result ?? { workspaceDir: fallbackEffects.workspace, agentDir: fallbackEffects.agentDir, sessionsDir: fallbackEffects.sessionsDir }, configBeforeDelete, nextConfig: committed.nextConfig }; } catch (error) { if (!(error instanceof AgentConfigPreconditionError)) throw error; const latestConfig = getRuntimeConfig(); if (listAgentEntries(latestConfig).some((agent) => agent.id === params.agentId)) throw params.onModified(); const effects = deletionEffects(latestConfig, params.agentId, params.fallbackWorkspace, params.stateDatabase?.env); return { agentRemoved: false, cleanupTargets: { workspaceDir: effects.workspace, agentDir: effects.agentDir, sessionsDir: effects.sessionsDir }, configBeforeDelete, nextConfig: latestConfig }; } } async function withClawAgentConfigRemoval(params, apply) { const expectedInstall = structuredClone(params.expectedInstall); const stateOptions = { ...params.stateDatabase, path: openOpenClawStateDatabase(params.stateDatabase).path }; return await withAgentDeletion(params.agentId, async (begin) => { const config = params.config ?? getRuntimeConfig(); assertAgentSessionStoreDeletionSafe(config, params.agentId, stateOptions); const effects = deletionEffects(config, params.agentId, params.fallbackWorkspace, stateOptions.env); const matchesInstall = (database) => expectedInstall === void 0 || isDeepStrictEqual(readClawInstallRecordFromDatabase(database.db, params.agentId) ?? null, expectedInstall); const { existingJournal, deletion } = runOpenClawStateWriteTransaction((database) => { if (!matchesInstall(database)) throw params.onModified(); const previousJournal = readAgentDeletionJournalInDatabase(database, params.agentId); return { existingJournal: previousJournal, deletion: begin({ agentId: params.agentId, workspaceDir: effects.workspace, agentDir: effects.agentDir, sessionsDir: effects.sessionsDir, deleteFiles: previousJournal?.deleteFiles ?? false }) }; }, stateOptions); let committed = false; let monitorEffectsStarted = false; const assertCurrent = (database) => { const check = (current) => { deletion.assertCurrent(current); if (!matchesInstall(current)) throw new Error(`Claw removal no longer owns agent ${params.agentId}.`); }; if (database) check(database); else { const current = openOpenClawStateDatabase(stateOptions); runSqliteDeferredTransactionSync(current.db, () => check(current)); } }; try { if (params.quiesceMonitors) { monitorEffectsStarted = true; await params.quiesceMonitors(deletion.entry.operationId); } assertCurrent(); await prepareAgentDeleteDatabases(config, params.agentId, effects.agentDir, stateOptions); assertCurrent(); return await apply(async () => { assertCurrent(); const result = await withAgentExecApprovalsRemoved(params.agentId, async () => commitClawAgentConfigRemoval({ ...params, config, stateDatabase: stateOptions }, assertCurrent), stateOptions); committed = true; assertCurrent(); return { ...result, operationId: deletion.entry.operationId, assertCurrent, drainMonitors: async () => { assertCurrent(); await params.drainMonitors?.(deletion.entry.operationId); assertCurrent(); }, runDatabaseCleanup: deletion.runDatabaseCleanup, completeDeletion: deletion.completeInTransaction }; }, assertCurrent); } finally { if (!committed && !monitorEffectsStarted && !existingJournal) deletion.rollback(); if (expectedInstall) runOpenClawStateWriteTransaction((database) => { try { assertCurrent(database); } catch { return; } updateClawInstallRecordStatus(params.agentId, "partial", { ...stateOptions, database, deletionOperation: deletion }); }, stateOptions); } }, stateOptions); } //#endregion //#region src/claws/lifecycle-mcp-removal.ts async function removeClawMcpServers(params) { const listed = params.options.sourceMcpServers ? void 0 : params.options.listMcpServers ? await params.options.listMcpServers() : params.options.config ? void 0 : await listConfiguredMcpServers(); params.assertCurrent(); if (listed && !listed.ok) throw new ClawRemoveError("mcp_config_unavailable", listed.error); const configured = listed?.ok ? listed.mcpServers : normalizeConfiguredMcpServers(params.options.sourceMcpServers ?? params.options.config?.mcp?.servers); const unsetMcpServer = params.options.unsetMcpServer ?? unsetConfiguredMcpServer; const mcpServers = []; for (const server of params.servers) { let removalError; params.assertCurrent(); await withClawMcpLifecycleLease(server.name, params.options, async (assertMcpCurrent) => { const assertCurrent = () => { params.assertCurrent(); assertMcpCurrent(); }; assertCurrent(); const currentRef = readClawMcpServerRefsByName(server.name, params.options).find((candidate) => candidate.agentId === params.agentId); if (!currentRef) throw new ClawRemoveError("mcp_cleanup_changed", `MCP ownership for ${JSON.stringify(server.name)} changed during removal.`); if (planClawMcpServerRemoval(currentRef, params.options).action === "release") { assertCurrent(); deleteClawMcpServerRef(params.agentId, server.name, params.options); mcpServers.push({ name: server.name, action: server.state === "missing" ? "missing" : "released" }); return; } const expectedServer = configured[server.name]; if (!expectedServer) { if (server.state === "present") throw new ClawRemoveError("mcp_cleanup_changed", `MCP server ${JSON.stringify(server.name)} disappeared during removal.`); assertCurrent(); deleteClawMcpServerRef(params.agentId, server.name, params.options); mcpServers.push({ name: server.name, action: "missing" }); return; } if (digestClawMcpServer(expectedServer) !== currentRef.configDigest) throw new ClawRemoveError("mcp_cleanup_changed", `MCP server ${JSON.stringify(server.name)} changed during removal.`); try { const result = await unsetMcpServer({ name: server.name, expectedServer, recordIndependentOwner: false, assertCurrent }); if (!result.ok) throw new Error(result.error); assertCurrent(); deleteClawMcpServerRef(params.agentId, server.name, params.options); mcpServers.push({ name: server.name, action: result.removed ? "removed" : "missing" }); } catch (cause) { const message = coerceErrorMessage(cause); mcpServers.push({ name: server.name, action: "error", message }); removalError = message; } }); params.assertCurrent(); if (removalError) return { mcpServers, error: removalError }; } return { mcpServers }; } //#endregion //#region src/claws/lifecycle-remove-state-blockers.ts function clawRemoveStateBlockers(record) { const blockers = []; if (record?.agentState === "modified") blockers.push({ code: "agent_modified", message: `Agent ${JSON.stringify(record.install.agentId)} changed after add.` }); for (const file of record?.workspaceFiles ?? []) if (file.state === "unsafe") blockers.push({ code: "workspace_file_unsafe", message: `${file.path}: ${file.message ?? "unsafe file"}` }); if (record && clawBootstrapStateBlocksRemove(record)) blockers.push({ code: "bootstrap_cleanup_uncertain", message: `BOOTSTRAP.md has ${record.bootstrap.state} ownership state and must be reconciled before removal.` }); for (const server of record?.mcpServers ?? []) if (server.state === "pending") blockers.push({ code: "mcp_cleanup_uncertain", message: `MCP server ${JSON.stringify(server.name)} has ${server.state} ownership state and must be reconciled before removal.` }); for (const cron of record?.cronJobs ?? []) if (cron.status !== "removed" && (cron.status !== "complete" || !cron.schedulerJobId)) blockers.push({ code: "cron_cleanup_uncertain", message: `Cron declaration ${JSON.stringify(cron.manifestId)} has ${cron.status} ownership state and must be reconciled before removal.` }); return blockers; } //#endregion //#region src/claws/package-remove-phase.ts async function applyClawPackageRemovalPhase(decisions, options) { const ordered = orderClawPackageRemovals(decisions); options.assertCurrent(); const cleanup = normalizeClawPackageCleanup(filterReferencedCleanup(options.referencedCleanup, "package")); if (!ordered.some((decision) => decision.packageRef.kind === "plugin" && decision.action === "uninstall")) return await applyClawPackageRemovals(ordered, { ...options, deps: options.packageDeps }); if (!options.packageGateway) throw new Error("Plugin cleanup requires the serving Gateway package owner."); const removed = await options.packageGateway({ agentId: options.agentId, operationId: options.operationId, expectedInstallDigest: digestClawRemovalInstall(readClawInstallRecord(options.agentId, options)), expectedPackagePlanDigest: digestClawPackageRemovalPlan(ordered, cleanup), cleanup }); options.assertCurrent(); const expected = ordered.map(({ packageRef }) => [ packageRef.kind, packageRef.ref, packageRef.version ]); const actual = removed.packages.map((pkg) => [ pkg.kind, pkg.ref, pkg.version ]); if (!isDeepStrictEqual(expected, actual) || removed.packages.some((pkg) => pkg.kind === "plugin" && pkg.action === "uninstalled") && !removed.application) throw new Error("Gateway package cleanup returned incomplete removal outcomes."); return removed; } //#endregion //#region src/claws/lifecycle-state.ts async function buildClawRemovePlan(target, options = {}) { const status = await readClawStatus(target, options); const blockers = []; if (status.records.length === 0) blockers.push({ code: "claw_not_found", message: `No installed Claw matches ${JSON.stringify(target)}.` }); else if (status.records.length > 1) blockers.push({ code: "claw_ambiguous", message: `Claw name ${JSON.stringify(target)} matches multiple agents; use an agent id.` }); const record = status.records.length === 1 ? status.records[0] : void 0; if (record?.install.agentOrigin === "adopted") return buildClawAdoptedRemovePlan(target, record, blockers); blockers.push(...clawRemoveStateBlockers(record)); const actions = []; if (record) { const packageCleanup = filterReferencedCleanup(options.referencedCleanup, "package"); const mcpCleanup = filterReferencedCleanup(options.referencedCleanup, "mcp"); const packageDecisions = await planClawPackageRemovals(record.install, record.packages, { ...options, deps: options.packageDeps, referencedCleanup: packageCleanup }); const packagePlan = projectClawPackageRemovePlan({ decisions: packageDecisions, inspections: record.packages, cleanup: packageCleanup }); blockers.push(...packagePlan.blockers); const config = options.config ?? getRuntimeConfig(); try { assertAgentSessionStoreDeletionSafe(config, record.install.agentId, options); } catch (error) { if (!(error instanceof AgentSharedStoreOwnerError)) throw error; blockers.push({ code: "shared_session_store_owner", message: error.message }); } const effects = deletionEffects(config, record.install.agentId, record.install.workspace, options.env); const survivingDatabaseFilePaths = resolveSurvivingDatabaseFilePaths(readAgentDeleteDatabaseRegistry(options), record.install.agentId, options.env); const sharedWithSurvivor = (pathname) => isPathOwnedBySurvivingAgent(config, record.install.agentId, pathname, survivingDatabaseFilePaths, options.env); const sharedWorkspace = Boolean(effects.workspace) && sharedWithSurvivor(effects.workspace); const sharedAgentDir = sharedWithSurvivor(effects.agentDir); const sharedSessionsDir = sharedWithSurvivor(effects.sessionsDir); const workspaceHasModifiedFiles = record.workspaceFiles.some((file) => file.state === "modified") || record.bootstrap.state === "modified"; const trackedWorkspacePaths = [...record.workspaceFiles.map((file) => file.path), ...record.install.bootstrap && record.bootstrap.state === "pending" ? [record.bootstrap.path] : []]; const workspaceHasUntrackedEntries = await workspaceContainsUntrackedEntries(record.install.workspace, trackedWorkspacePaths); const { attachedJobs, monitors, inspectionUnavailable } = await readClawRemoveCronInventory(record.install.agentId, options); const ownedSchedulerJobIds = new Set(record.cronJobs.filter((cron) => cron.status !== "removed" && cron.schedulerJobId).map((cron) => cron.schedulerJobId)); actions.push({ kind: "agent", id: record.install.agentId, action: "remove", target: `agents.entries[${JSON.stringify(record.install.agentId)}]`, blocked: record.agentState === "modified", details: { expectedState: record.agentState, configDigest: record.install.agentConfigDigest, removalSurfaceDigest: digestClawAgentRemovalSurface(options.config ?? getRuntimeConfig(), record.install.agentId), ownedPaths: record.install.agentOwnedPaths }, ...record.agentState === "modified" ? { reason: "Agent config digest changed." } : {} }); if (effects.pruned.removedBindings > 0) actions.push({ kind: "configBinding", id: record.install.agentId, action: "remove", target: `bindings[agentId=${record.install.agentId}]`, blocked: record.agentState === "modified", details: { count: effects.pruned.removedBindings } }); if (effects.pruned.removedAllow > 0) actions.push({ kind: "agentAllow", id: record.install.agentId, action: "remove", target: `tools.agentToAgent.allow[${record.install.agentId}]`, blocked: record.agentState === "modified", details: { count: effects.pruned.removedAllow } }); if (effects.workspace) actions.push({ kind: "workspace", id: record.install.agentId, action: sharedWorkspace || workspaceHasModifiedFiles || workspaceHasUntrackedEntries ? "retain" : "trash", target: effects.workspace, blocked: record.agentState === "modified", details: { retained: sharedWorkspace || workspaceHasModifiedFiles || workspaceHasUntrackedEntries, sharedWith: effects.workspaceSharedWith }, ...sharedWorkspace ? { reason: "Workspace contains state owned by another agent." } : workspaceHasModifiedFiles ? { reason: "Workspace contains locally modified Claw-managed files." } : workspaceHasUntrackedEntries ? { reason: "Workspace contains files or directories not managed by this Claw." } : {} }); if (effects.agentDir) actions.push({ kind: "agentState", id: record.install.agentId, action: sharedAgentDir ? "retain" : "trash", target: effects.agentDir, blocked: record.agentState === "modified", ...sharedAgentDir ? { reason: "Agent directory contains state owned by another agent." } : {} }); actions.push({ kind: "sessionIndex", id: record.install.agentId, action: "delete", target: `session store entries for agent:${record.install.agentId}`, blocked: record.agentState === "modified" }); actions.push({ kind: "sessionTranscripts", id: record.install.agentId, action: sharedSessionsDir ? "retain" : "trash", target: effects.sessionsDir, blocked: record.agentState === "modified", ...sharedSessionsDir ? { reason: "Session directory contains state owned by another agent." } : {} }); for (const job of attachedJobs.filter((candidate) => !ownedSchedulerJobIds.has(candidate.id))) { if (monitors.some((monitor) => isDeepStrictEqual(monitor, job))) { actions.push({ kind: "scheduledJob", id: job.id, action: "remove", target: `cron_jobs:${job.id}`, blocked: false, reason: "Config-owned monitor; Gateway cancellation and drainage precede local cleanup.", details: { ...job } }); continue; } blockers.push({ code: "agent_job_attached", message: `Cron job ${JSON.stringify(job.id)} still references agent ${JSON.stringify(record.install.agentId)}; reassign or remove independent work, or reconnect to the serving Gateway to verify monitor ownership.` }); actions.push({ kind: "scheduledJob", id: job.id, action: "retain", target: `cron_jobs:${job.id}`, blocked: true, reason: "Scheduled work without verified Claw or config ownership must be handled explicitly.", details: { ...inspectionUnavailable ? { monitorInspection: "unavailable" } : {}, name: job.name, enabled: job.enabled, agentId: job.agentId, ownerAgentId: job.ownerAgentId } }); } for (const file of record.workspaceFiles) actions.push({ kind: "workspaceFile", id: file.path, action: file.state === "unchanged" ? "delete" : "retain", target: `${file.workspace}:${file.path}`, blocked: file.state === "unsafe", details: { expectedState: file.state, contentDigest: file.contentDigest, workspace: file.workspace }, ...file.state === "modified" ? { reason: "Local content changed; preserve the file." } : {} }); const bootstrapAction = planClawBootstrapRemoval(record); if (bootstrapAction) actions.push(bootstrapAction); actions.push(...packagePlan.actions); const unmatchedMcpSelectors = new Set(mcpCleanup?.selected ?? []); for (const server of record.mcpServers) { const blocked = server.state === "pending"; const decision = planClawMcpServerRemoval(server, { ...options, referencedCleanup: mcpCleanup }); unmatchedMcpSelectors.delete(clawMcpRemovalSelector(server)); if (decision.blocked) blockers.push({ code: "referenced_cleanup_requires_override", message: `${clawMcpRemovalSelector(server)}: ${decision.reason ?? "explicit conflict override is required"}` }); actions.push({ kind: "mcpServer", id: server.name, action: blocked ? "retain" : decision.action, target: `mcp.servers.${server.name}`, blocked, details: { expectedState: server.state, configDigest: server.configDigest, relationship: server.relationship, origin: server.origin, independentOwner: server.independentOwner, affectedClawAgentIds: decision.affectedClawAgentIds, cleanupMode: mcpCleanup?.mode ?? "retain", availableCleanupModes: server.relationship === "referenced" ? [ "retain", "remove-if-unused", "remove-selected" ] : ["remove"] }, ...blocked ? { reason: `MCP ownership state is ${server.state}.` } : decision.reason ? { reason: decision.reason } : {} }); } for (const selector of unmatchedMcpSelectors) blockers.push({ code: "referenced_cleanup_not_found", message: `Selected referenced resource ${JSON.stringify(selector)} is not owned by this Claw.` }); for (const cron of record.cronJobs) { const blocked = cron.status !== "removed" && (cron.status !== "complete" || !cron.schedulerJobId); actions.push({ kind: "cronJob", id: cron.manifestId, action: blocked ? "retain" : "remove", target: cron.schedulerJobId ?? cron.declarationKey, blocked, details: { expectedStatus: cron.status, declarationKey: cron.declarationKey, schedulerJobId: cron.schedulerJobId, job: cron.job }, ...blocked ? { reason: `Cron ownership state is ${cron.status}.` } : {} }); } actions.push({ kind: "installRecord", id: record.install.agentId, action: "remove", target: `claw_installs:${record.install.agentId}`, blocked: false, details: { expectedStatus: record.install.status, planIntegrity: record.install.planIntegrity, sourceIntegrity: record.install.claw.integrity } }); } const planIdentity = { target, agentId: record?.install.agentId, actions, blockers }; return { schemaVersion: CLAW_REMOVE_PLAN_SCHEMA_VERSION, stability: CLAW_OUTPUT_STABILITY, dryRun: true, mutationAllowed: false, planIntegrity: digestClawValue(planIdentity), target, ...record ? { agentId: record.install.agentId } : {}, actions, blockers }; } async function applyClawRemovePlan(plan, options = {}) { if (options.consentPlanIntegrity !== plan.planIntegrity) throw new ClawRemoveError("plan_integrity_mismatch", "Consent does not match the current Claw remove plan; run remove --dry-run again."); if (plan.blockers.length > 0 || !plan.agentId) throw new ClawRemoveError("remove_blocked", "The Claw remove plan contains blockers."); if (plan.actions.some((action) => action.kind === "installRecord" && action.action === "release")) return await applyClawAdoptedRemovePlan(plan, options); const monitorGateway = options.monitorGateway; if (!monitorGateway) throw new ClawRemoveError("monitor_gateway_required", "Claw removal requires the serving Gateway to establish safe cancellation and drainage."); const currentPlan = await buildClawRemovePlan(plan.target, options); if (currentPlan.planIntegrity !== plan.planIntegrity) throw new ClawRemoveError("remove_changed", "Claw-owned state changed after remove planning."); const agentId = plan.agentId; const record = (await readClawStatus(plan.agentId, options)).records[0]; const expectedRemovalSurfaceDigest = plan.actions.find((action) => action.kind === "agent" && action.id === agentId)?.details?.removalSurfaceDigest; if (typeof expectedRemovalSurfaceDigest !== "string") throw new ClawRemoveError("remove_changed", "Claw remove plan is missing config state."); if (!record || record.agentState === "modified" || clawBootstrapStateBlocksRemove(record) || record.workspaceFiles.some((file) => file.state === "unsafe") || record.mcpServers.some((server) => server.state === "pending")) throw new ClawRemoveError("remove_changed", "Claw-owned state changed after remove planning."); const packageDecisions = await planClawPackageRemovals(record.install, record.packages, { ...options, deps: options.packageDeps, referencedCleanup: filterReferencedCleanup(options.referencedCleanup, "package") }); const plannedPackages = plan.actions.filter((action) => action.kind === "packageRef").map((action) => `${action.id}:${action.action}`).toSorted(); const currentPackages = packageDecisions.map((decision) => `${decision.packageRef.kind}:${decision.packageRef.ref}@${decision.packageRef.version}:${decision.action === "uninstall" ? "uninstall" : "release"}`).toSorted(); if (JSON.stringify(plannedPackages) !== JSON.stringify(currentPackages)) throw new ClawRemoveError("remove_changed", "Package ownership changed after remove planning."); const plannedMcpServers = plan.actions.filter((action) => action.kind === "mcpServer").map((action) => `${action.id}:${action.action}`).toSorted(); const currentMcpServers = record.mcpServers.map((server) => `${server.name}:${planClawMcpServerRemoval(server, options).action}`).toSorted(); if (JSON.stringify(plannedMcpServers) !== JSON.stringify(currentMcpServers)) throw new ClawRemoveError("remove_changed", "MCP ownership changed after remove planning."); const result = { schemaVersion: CLAW_REMOVE_RESULT_SCHEMA_VERSION, stability: CLAW_OUTPUT_STABILITY, dryRun: false, status: "partial", agentId, agentRemoved: false, workspaceFiles: [], packages: [], mcpServers: [], cronJobs: [], packageRefsReleased: 0 }; const partial = (code, message) => ({ ...result, error: { code, message } }); const monitors = currentPlan.actions.filter((action) => action.kind === "scheduledJob" && action.action === "remove").map((action) => clawMonitorSnapshotSchema.parse(action.details)); return await withClawAgentConfigRemoval({ agentId, expectedDigest: record.install.agentConfigDigest, expectedInstall: record.orphaned ? null : record.install, expectedRemovalSurfaceDigest, expectedState: record.agentState, fallbackWorkspace: record.install.workspace, config: options.config, stateDatabase: options, onModified: () => new ClawRemoveError("agent_modified", "Agent config changed during remove."), quiesceMonitors: (operationId) => monitorGateway.quiesce(agentId, operationId, monitors), drainMonitors: async (operationId) => await monitorGateway.drain(agentId, operationId) }, async (commitRemoval, assertCurrent) => { assertCurrent(); const mcpRemoval = await removeClawMcpServers({ agentId, servers: record.mcpServers, options, assertCurrent }); assertCurrent(); result.mcpServers = mcpRemoval.mcpServers; if (mcpRemoval.error) return partial("mcp_cleanup_failed", mcpRemoval.error); const cronJobs = result.cronJobs; for (const cron of record.cronJobs) { if (cron.status !== "removed" && (!cron.schedulerJobId || cron.status !== "complete")) throw new ClawRemoveError("cron_cleanup_uncertain", `Cron declaration ${JSON.stringify(cron.manifestId)} is not safely removable.`); if (cron.status !== "removed" && (!options.cronGateway?.get || !options.cronGateway.remove)) throw new ClawRemoveError("cron_gateway_required", "Claw cron jobs require the gateway-owned cron.get and cron.remove APIs."); try { if (cron.status !== "removed") { const live = await options.cronGateway.get(cron.schedulerJobId); if (live != null && !clawCronGatewayJobMatchesRef(agentId, cron, live)) throw new Error(`Cron declaration ${JSON.stringify(cron.manifestId)} changed after planning.`); assertCurrent(); if (live != null) try { await options.cronGateway.remove(cron.schedulerJobId); } catch (removeError) { if (await options.cronGateway.get(cron.schedulerJobId) != null) throw removeError; } assertCurrent(); markClawCronRefRemoved(agentId, cron.manifestId, options); } deleteClawCronRef(agentId, cron.manifestId, options); cronJobs.push({ manifestId: cron.manifestId, schedulerJobId: cron.schedulerJobId, action: "removed" }); } catch (error) { const message = coerceErrorMessage(error); cronJobs.push({ manifestId: cron.manifestId, schedulerJobId: cron.schedulerJobId, action: "error", message }); return partial("cron_cleanup_failed", message); } } const configRemoval = await commitRemoval(); const { cleanupTargets, configBeforeDelete, completeDeletion } = configRemoval; result.agentRemoved = configRemoval.agentRemoved; try { await configRemoval.drainMonitors(); configRemoval.assertCurrent(); } catch (error) { return partial("monitor_cleanup_failed", coerceErrorMessage(error)); } const purgeFailed = await (options.purgeSessions ?? (await import("./cleanup-service-DF_cZ5gM.mjs")).purgeAgentSessionStoreEntries)(configBeforeDelete, agentId, { env: options.env, runDatabaseCleanup: configRemoval.runDatabaseCleanup }); assertCurrent(); if (purgeFailed) return partial("session_cleanup_failed", "Session cleanup failed; correct the reported error and retry Claw removal."); try { const removed = await applyClawPackageRemovalPhase(packageDecisions, { ...options, agentId, operationId: configRemoval.operationId, assertCurrent }); result.packages = removed.packages; result.pluginRuntime = removed.application; result.warnings = removed.warnings; } catch (error) { return partial("package_cleanup_failed", coerceErrorMessage(error)); } assertCurrent(); const packageErrors = result.packages.filter((pkg) => pkg.action === "error"); if (packageErrors.length > 0) return partial("package_cleanup_failed", packageErrors.map((pkg) => pkg.reason).join("; ")); const workspaceFiles = result.workspaceFiles; for (const file of record.workspaceFiles) { assertCurrent(); workspaceFiles.push(await removeClawWorkspaceFile(file, assertCurrent)); } assertCurrent(); const bootstrap = await removeClawBootstrap(record, assertCurrent); const cleanupErrors = workspaceFiles.filter((file) => file.action === "error").map((file) => file.message ?? `Could not remove ${file.path}.`); if (bootstrap?.action === "error") cleanupErrors.push(bootstrap.message ?? `Could not remove ${bootstrap.path}.`); if (cleanupErrors.length === 0) { const workspaceHasRemainingEntries = await workspaceContainsUntrackedEntries(cleanupTargets.workspaceDir, record.workspaceFiles.map((file) => file.path)); configRemoval.assertCurrent(); cleanupErrors.push(...await cleanupClawAgentFilesystem({ agentId, nextConfig: configRemoval.nextConfig, targets: cleanupTargets, runtime: clawRemoveQuietRuntime, trashPath: options.trashPath, stateDatabase: options, assertCurrent, retainWorkspace: workspaceHasRemainingEntries || bootstrap?.action === "retainedModified" || workspaceFiles.some((file) => file.action === "retainedModified") })); } const complete = releaseClawRemoveRows(agentId, workspaceFiles, cleanupErrors, configRemoval.assertCurrent, completeDeletion, options); return { ...result, status: complete ? "complete" : "partial", ...bootstrap ? { bootstrap } : {}, packageRefsReleased: complete ? record.packages.length : 0, ...complete ? {} : { error: { code: "workspace_cleanup_failed", message: cleanupErrors.join("; ") } } }; }).catch((error) => partial(error instanceof ClawRemoveError ? error.code : "monitor_cleanup_failed", coerceErrorMessage(error))); } //#endregion export { CLAW_REMOVE_RESULT_SCHEMA_VERSION as i, buildClawRemovePlan as n, CLAW_REMOVE_PLAN_SCHEMA_VERSION as r, applyClawRemovePlan as t };