Download src/claws/lifecycle.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 24.3 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/claws/lifecycle.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/claws/lifecycle.ts
-
curl -L -o lifecycle.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/claws/lifecycle.ts
24.3 kB
| // Builds complete read-only Claw add plans without mutating local state. | |
| import { createHash } from "node:crypto"; | |
| import { lstat, realpath } from "node:fs/promises"; | |
| import { homedir } from "node:os"; | |
| import { relative, resolve } from "node:path"; | |
| import { stableStringify } from "@openclaw/normalization-core"; | |
| import type { OpenClawConfig } from "../config/types.openclaw.js"; | |
| import { resolvePathViaExistingAncestorSync } from "../infra/boundary-path.js"; | |
| import { assertNoSymlinkParents } from "../infra/fs-safe-advanced.js"; | |
| import { FsSafeError, root as fsSafeRoot, type Root } from "../infra/fs-safe.js"; | |
| import { resolveUserPath } from "../utils.js"; | |
| import { | |
| clawAddCapabilityChange, | |
| clawAgentCapabilityChange, | |
| clawAgentConfigurationNotices, | |
| findClawExtensionPackageCollisions, | |
| planClawExtensions, | |
| } from "./application-plan.js"; | |
| import { digestClawMcpServer } from "./mcp.js"; | |
| import { clawManifestWorkspaceConflictsWithPath } from "./schema.js"; | |
| import { MAX_MANAGED_FILE_BYTES, MAX_MANAGED_WORKSPACE_BYTES } from "./source-limits.js"; | |
| import { materializeClawToolProfile } from "./tool-profile-consent.js"; | |
| import { | |
| CLAW_ADD_PLAN_SCHEMA_VERSION, | |
| CLAW_BOOTSTRAP_FILE_NAMES, | |
| CLAW_OUTPUT_STABILITY, | |
| type ClawAddPlan, | |
| type ClawAddPlanAction, | |
| type ClawAddCapabilityChange, | |
| type ClawDiagnostic, | |
| type ClawManifest, | |
| type ClawLocalPrerequisite, | |
| type ClawOpenClawProfile, | |
| type ClawPackagePreflight, | |
| type ClawPackagePreflightResult, | |
| type ClawSourceIdentity, | |
| type ClawWorkspaceSourceSnapshot, | |
| } from "./types.js"; | |
| const AGENT_ID_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/; | |
| export type ClawAddPlanContext = { | |
| config?: OpenClawConfig; | |
| agentId?: string; | |
| workspace?: string; | |
| resumableWorkspace?: string; | |
| existingAgentIds?: Iterable<string>; | |
| existingWorkspacePaths?: Iterable<string>; | |
| existingMcpServerNames?: Iterable<string>; | |
| existingMcpServers?: Record<string, Record<string, unknown>>; | |
| packagePreflight?: ClawPackagePreflight; | |
| sourceReferenceRoot?: string; | |
| }; | |
| function sourceReferencePath(root: string, path: string): string { | |
| return `${root.replace(/\/+$/u, "")}/${path.replaceAll("\\", "/")}`; | |
| } | |
| function canonicalWorkspacePath(value: string): string { | |
| return resolvePathViaExistingAncestorSync(resolve(resolveUserPath(value))); | |
| } | |
| function blocker(code: string, path: string, message: string): ClawDiagnostic { | |
| return { level: "error", code, phase: "plan", path, message }; | |
| } | |
| type PendingWorkspaceFileAction = { | |
| action: ClawAddPlanAction; | |
| sourcePath: string; | |
| manifestPath: string; | |
| byteLength: number; | |
| content?: Buffer; | |
| }; | |
| function blockedWorkspaceFileAction(params: { | |
| id: string; | |
| source: string; | |
| target: string; | |
| reason: string; | |
| }): ClawAddPlanAction { | |
| return { | |
| kind: "workspaceFile", | |
| id: params.id, | |
| action: "write", | |
| target: params.target, | |
| source: params.source, | |
| blocked: true, | |
| reason: params.reason, | |
| }; | |
| } | |
| function workspaceSourceErrorCode( | |
| error: unknown, | |
| ): "workspace_source_invalid" | "workspace_source_unsafe" | "workspace_source_too_large" { | |
| if (error instanceof FsSafeError) { | |
| if (error.code === "too-large") { | |
| return "workspace_source_too_large"; | |
| } | |
| if (error.code === "symlink" || error.code === "hardlink" || error.code === "path-mismatch") { | |
| return "workspace_source_unsafe"; | |
| } | |
| } | |
| if (error instanceof Error && error.message.includes("symlinked directory")) { | |
| return "workspace_source_unsafe"; | |
| } | |
| return "workspace_source_invalid"; | |
| } | |
| function workspaceSourceMessage(code: string, sourcePath: string): string { | |
| if (code === "workspace_source_too_large") { | |
| return `Workspace source ${JSON.stringify(sourcePath)} exceeds ${MAX_MANAGED_FILE_BYTES} bytes.`; | |
| } | |
| if (code === "workspace_sources_too_large") { | |
| return `Workspace sources exceed ${MAX_MANAGED_WORKSPACE_BYTES} aggregate bytes.`; | |
| } | |
| if (code === "workspace_source_unsafe") { | |
| return `Workspace source ${JSON.stringify(sourcePath)} must be a regular, non-symlinked, non-hardlinked file.`; | |
| } | |
| return `Workspace source ${JSON.stringify(sourcePath)} must resolve to a file inside the Claw package.`; | |
| } | |
| async function inspectWorkspaceFileAction(params: { | |
| sourceRoot: Root; | |
| source: ClawSourceIdentity; | |
| workspace: string; | |
| sourcePath: string; | |
| targetPath: string; | |
| id: string; | |
| manifestPath: string; | |
| }): Promise<{ | |
| pending?: PendingWorkspaceFileAction; | |
| action?: ClawAddPlanAction; | |
| blocker?: ClawDiagnostic; | |
| }> { | |
| const requestedSource = resolve(params.source.packageRoot, params.sourcePath); | |
| const requestedTarget = resolve(params.workspace, params.targetPath); | |
| try { | |
| await assertNoSymlinkParents({ | |
| rootDir: params.source.packageRoot, | |
| targetPath: requestedSource, | |
| allowMissing: false, | |
| messagePrefix: "Workspace source", | |
| }); | |
| const opened = await params.sourceRoot.open(params.sourcePath, { | |
| hardlinks: "reject", | |
| symlinks: "reject", | |
| }); | |
| await opened[Symbol.asyncDispose](); | |
| if (opened.stat.size > MAX_MANAGED_FILE_BYTES) { | |
| throw new FsSafeError( | |
| "too-large", | |
| `file exceeds limit of ${MAX_MANAGED_FILE_BYTES} bytes (got ${opened.stat.size})`, | |
| ); | |
| } | |
| return { | |
| pending: { | |
| sourcePath: params.sourcePath, | |
| manifestPath: params.manifestPath, | |
| byteLength: opened.stat.size, | |
| action: { | |
| kind: "workspaceFile", | |
| id: params.id, | |
| action: "write", | |
| target: requestedTarget, | |
| source: opened.realPath, | |
| details: { expectedState: "absent" }, | |
| blocked: false, | |
| }, | |
| }, | |
| }; | |
| } catch (error) { | |
| const code = workspaceSourceErrorCode(error); | |
| const message = workspaceSourceMessage(code, params.sourcePath); | |
| const diagnostic = blocker(code, params.manifestPath, message); | |
| return { | |
| action: blockedWorkspaceFileAction({ | |
| id: params.id, | |
| target: requestedTarget, | |
| source: requestedSource, | |
| reason: diagnostic.message, | |
| }), | |
| blocker: diagnostic, | |
| }; | |
| } | |
| } | |
| export async function buildClawAddPlan(params: { | |
| manifest: ClawManifest; | |
| clawMarkdownBody?: Buffer; | |
| packageBootstrap?: ClawWorkspaceSourceSnapshot; | |
| includePackageBootstrap?: boolean; | |
| openClawProfile?: ClawOpenClawProfile; | |
| reconstructLegacyDynamicToolProfilePlan?: boolean; | |
| source: ClawSourceIdentity; | |
| diagnostics?: ClawDiagnostic[]; | |
| context?: ClawAddPlanContext; | |
| }): Promise<ClawAddPlan> { | |
| const context = params.context ?? {}; | |
| const finalId = context.agentId ?? params.manifest.agent.id; | |
| const workspace = canonicalWorkspacePath( | |
| context.workspace ?? resolve(homedir(), ".openclaw", `workspace-${finalId}`), | |
| ); | |
| const packageRoot = await realpath(params.source.packageRoot).catch( | |
| () => params.source.packageRoot, | |
| ); | |
| const manifestPath = resolvePathViaExistingAncestorSync(resolve(params.source.manifestPath)); | |
| const source = { ...params.source, packageRoot, manifestPath }; | |
| const planSource = context.sourceReferenceRoot | |
| ? { | |
| ...source, | |
| packageRoot: context.sourceReferenceRoot, | |
| manifestPath: sourceReferencePath( | |
| context.sourceReferenceRoot, | |
| relative(packageRoot, manifestPath), | |
| ), | |
| } | |
| : source; | |
| const planSourcePath = (path: string, fallback: string): string => | |
| context.sourceReferenceRoot ? sourceReferencePath(context.sourceReferenceRoot, path) : fallback; | |
| const sourceRoot = await fsSafeRoot(packageRoot); | |
| const blockers: ClawDiagnostic[] = []; | |
| const actions: ClawAddPlanAction[] = []; | |
| const capabilityChanges: ClawAddCapabilityChange[] = []; | |
| const readinessRequirements: ClawLocalPrerequisite[] = []; | |
| if (!AGENT_ID_PATTERN.test(finalId)) { | |
| blockers.push( | |
| blocker( | |
| "invalid_agent_id", | |
| "$.agent.id", | |
| `Final agent id ${JSON.stringify(finalId)} is not a valid portable agent id.`, | |
| ), | |
| ); | |
| } | |
| const existingAgentIds = new Set(context.existingAgentIds ?? []); | |
| const agentBlocked = existingAgentIds.has(finalId); | |
| const openClawAgentSettings = params.openClawProfile?.agent ?? {}; | |
| const persistedOpenClawAgentSettings = params.reconstructLegacyDynamicToolProfilePlan | |
| ? openClawAgentSettings | |
| : materializeClawToolProfile(openClawAgentSettings); | |
| const agentConfig: ClawAddPlan["agent"]["config"] = { | |
| ...params.manifest.agent, | |
| ...persistedOpenClawAgentSettings, | |
| id: finalId, | |
| workspace, | |
| }; | |
| if (agentBlocked) { | |
| blockers.push( | |
| blocker( | |
| "agent_id_collision", | |
| "$.agent.id", | |
| `Agent id ${JSON.stringify(finalId)} already exists; Claws never merge into existing agents.`, | |
| ), | |
| ); | |
| } | |
| actions.push({ | |
| kind: "agent", | |
| id: finalId, | |
| action: "create", | |
| target: `agents.entries[${JSON.stringify(finalId)}]`, | |
| details: { ...agentConfig, expectedState: "absent" }, | |
| blocked: agentBlocked || !AGENT_ID_PATTERN.test(finalId), | |
| }); | |
| const agentCapability = clawAgentCapabilityChange(finalId, openClawAgentSettings); | |
| if (agentCapability) { | |
| capabilityChanges.push(agentCapability); | |
| } | |
| const configuredWorkspacePaths = new Set( | |
| [...(context.existingWorkspacePaths ?? [])].map((path) => canonicalWorkspacePath(path)), | |
| ); | |
| const configuredWorkspaceConflict = configuredWorkspacePaths.has(workspace); | |
| const workspaceExistsOnDisk = await lstat(workspace) | |
| .then(() => true) | |
| .catch(() => false); | |
| const resumableWorkspace = context.resumableWorkspace | |
| ? canonicalWorkspacePath(context.resumableWorkspace) | |
| : undefined; | |
| const workspaceBlocked = | |
| configuredWorkspaceConflict || (workspaceExistsOnDisk && resumableWorkspace !== workspace); | |
| if (workspaceBlocked) { | |
| blockers.push( | |
| blocker( | |
| "workspace_collision", | |
| "$.workspace", | |
| `Workspace ${JSON.stringify(workspace)} already exists; a Claw requires a new workspace.`, | |
| ), | |
| ); | |
| } | |
| actions.push({ | |
| kind: "workspace", | |
| id: finalId, | |
| action: "create", | |
| target: workspace, | |
| details: { expectedState: "absent" }, | |
| blocked: workspaceBlocked, | |
| ...(workspaceBlocked | |
| ? { reason: `Workspace ${JSON.stringify(workspace)} already exists.` } | |
| : {}), | |
| }); | |
| if (params.packageBootstrap && params.includePackageBootstrap !== false) { | |
| actions.push({ | |
| kind: "bootstrap", | |
| id: "BOOTSTRAP.md", | |
| action: "write", | |
| target: resolve(workspace, "BOOTSTRAP.md"), | |
| source: planSourcePath(params.packageBootstrap.sourcePath, params.packageBootstrap.realPath), | |
| digest: params.packageBootstrap.digest, | |
| details: { | |
| sourcePath: params.packageBootstrap.sourcePath, | |
| byteLength: params.packageBootstrap.byteLength, | |
| expectedState: "absent-or-native-consumed", | |
| lifecycle: "native-seed-once", | |
| }, | |
| blocked: workspaceBlocked, | |
| ...(workspaceBlocked | |
| ? { reason: `Workspace ${JSON.stringify(workspace)} already exists.` } | |
| : {}), | |
| }); | |
| } | |
| const pendingWorkspaceFiles: PendingWorkspaceFileAction[] = []; | |
| async function addWorkspaceFileInspection(fileParams: { | |
| sourcePath: string; | |
| targetPath: string; | |
| id: string; | |
| manifestPath: string; | |
| }): Promise<void> { | |
| const result = await inspectWorkspaceFileAction({ | |
| sourceRoot, | |
| source, | |
| workspace, | |
| sourcePath: fileParams.sourcePath, | |
| targetPath: fileParams.targetPath, | |
| id: fileParams.id, | |
| manifestPath: fileParams.manifestPath, | |
| }); | |
| const action = result.pending?.action ?? result.action; | |
| if (!action) { | |
| throw new Error("Claw workspace source inspection did not produce an action"); | |
| } | |
| if (action.source) { | |
| action.source = planSourcePath(fileParams.sourcePath, action.source); | |
| } | |
| action.blocked ||= workspaceBlocked; | |
| if (workspaceBlocked) { | |
| action.reason = `Workspace ${JSON.stringify(workspace)} already exists.`; | |
| } | |
| actions.push(action); | |
| if (result.pending) { | |
| pendingWorkspaceFiles.push(result.pending); | |
| } | |
| if (result.blocker) { | |
| blockers.push(result.blocker); | |
| } | |
| } | |
| if (params.clawMarkdownBody && params.clawMarkdownBody.toString("utf8").trim().length > 0) { | |
| if (clawManifestWorkspaceConflictsWithPath(params.manifest, "SOUL.md")) { | |
| const diagnostic = blocker( | |
| "claw_body_soul_conflict", | |
| "$.workspace", | |
| "CLAW.md body content and an explicit SOUL.md workspace declaration cannot both be present.", | |
| ); | |
| blockers.push(diagnostic); | |
| actions.push({ | |
| kind: "workspaceFile", | |
| id: "SOUL.md", | |
| action: "write", | |
| target: resolve(workspace, "SOUL.md"), | |
| source: planSource.manifestPath, | |
| sourceKind: "clawMarkdownBody", | |
| blocked: true, | |
| reason: diagnostic.message, | |
| }); | |
| } else { | |
| const pending: PendingWorkspaceFileAction = { | |
| sourcePath: source.manifestPath, | |
| manifestPath: "$body", | |
| byteLength: params.clawMarkdownBody.byteLength, | |
| content: params.clawMarkdownBody, | |
| action: { | |
| kind: "workspaceFile", | |
| id: "SOUL.md", | |
| action: "write", | |
| target: resolve(workspace, "SOUL.md"), | |
| source: planSource.manifestPath, | |
| sourceKind: "clawMarkdownBody", | |
| details: { expectedState: "absent" }, | |
| blocked: false, | |
| }, | |
| }; | |
| pendingWorkspaceFiles.push(pending); | |
| actions.push(pending.action); | |
| } | |
| } | |
| for (const name of CLAW_BOOTSTRAP_FILE_NAMES) { | |
| const declaration = params.manifest.workspace.bootstrapFiles[name]; | |
| if (!declaration) { | |
| continue; | |
| } | |
| await addWorkspaceFileInspection({ | |
| sourcePath: declaration.source, | |
| targetPath: name, | |
| id: name, | |
| manifestPath: `$.workspace.bootstrapFiles.${name}`, | |
| }); | |
| } | |
| for (const [index, file] of params.manifest.workspace.files.entries()) { | |
| await addWorkspaceFileInspection({ | |
| sourcePath: file.source, | |
| targetPath: file.path, | |
| id: file.path, | |
| manifestPath: `$.workspace.files[${index}]`, | |
| }); | |
| } | |
| const workspaceByteLength = pendingWorkspaceFiles.reduce( | |
| (total, pending) => total + pending.byteLength, | |
| 0, | |
| ); | |
| if (workspaceByteLength > MAX_MANAGED_WORKSPACE_BYTES) { | |
| const diagnostic = blocker( | |
| "workspace_sources_too_large", | |
| "$.workspace", | |
| workspaceSourceMessage("workspace_sources_too_large", ""), | |
| ); | |
| blockers.push(diagnostic); | |
| for (const pending of pendingWorkspaceFiles) { | |
| pending.action.blocked = true; | |
| pending.action.reason = diagnostic.message; | |
| } | |
| } else { | |
| for (const pending of pendingWorkspaceFiles) { | |
| if (pending.content) { | |
| pending.action.digest = `sha256:${createHash("sha256").update(pending.content).digest("hex")}`; | |
| continue; | |
| } | |
| try { | |
| await assertNoSymlinkParents({ | |
| rootDir: source.packageRoot, | |
| targetPath: resolve(source.packageRoot, pending.sourcePath), | |
| allowMissing: false, | |
| messagePrefix: "Workspace source", | |
| }); | |
| const read = await sourceRoot.read(pending.sourcePath, { | |
| hardlinks: "reject", | |
| maxBytes: MAX_MANAGED_FILE_BYTES, | |
| symlinks: "reject", | |
| }); | |
| pending.action.source = planSourcePath(pending.sourcePath, read.realPath); | |
| pending.action.digest = `sha256:${createHash("sha256").update(read.buffer).digest("hex")}`; | |
| } catch (error) { | |
| const code = workspaceSourceErrorCode(error); | |
| const message = workspaceSourceMessage(code, pending.sourcePath); | |
| const diagnostic = blocker(code, pending.manifestPath, message); | |
| pending.action.blocked = true; | |
| pending.action.reason = diagnostic.message; | |
| blockers.push(diagnostic); | |
| } | |
| } | |
| } | |
| for (const [index, pkg] of params.manifest.packages.entries()) { | |
| const preflight: ClawPackagePreflightResult = context.packagePreflight | |
| ? await context.packagePreflight(pkg, workspace) | |
| : { | |
| ok: false, | |
| code: "package_install_unavailable", | |
| message: "Package preflight is unavailable.", | |
| }; | |
| const diagnostic = preflight.ok | |
| ? undefined | |
| : blocker( | |
| preflight.code ?? "package_install_unavailable", | |
| `$.packages[${index}]`, | |
| preflight.message ?? "Package preflight failed.", | |
| ); | |
| if (diagnostic) { | |
| blockers.push(diagnostic); | |
| } | |
| if (preflight.ok && preflight.requirements) { | |
| readinessRequirements.push(...preflight.requirements); | |
| } | |
| actions.push({ | |
| kind: "package", | |
| id: `${pkg.kind}:${pkg.ref}`, | |
| action: preflight.ok && preflight.action === "reuse" ? "reuse" : "install", | |
| target: `${pkg.source}:${pkg.ref}@${pkg.version}`, | |
| digest: preflight.integrity, | |
| details: { | |
| ...pkg, | |
| ...(preflight.integrity ? { integrity: preflight.integrity } : {}), | |
| ...(preflight.installId ? { installId: preflight.installId } : {}), | |
| ...(preflight.warning ? { riskWarning: preflight.warning } : {}), | |
| ...(preflight.requirements ? { prerequisites: preflight.requirements } : {}), | |
| expectedState: !preflight.ok | |
| ? "unresolved" | |
| : preflight.action === "reuse" | |
| ? "present-exact" | |
| : "absent", | |
| ownerAction: preflight.action, | |
| requirementState: !preflight.ok | |
| ? "conflicting" | |
| : preflight.action === "install" | |
| ? "missing-installable" | |
| : preflight.requirements && preflight.requirements.length > 0 | |
| ? "setup-required" | |
| : "satisfied", | |
| }, | |
| blocked: !preflight.ok, | |
| ...(diagnostic ? { reason: diagnostic.message } : {}), | |
| }); | |
| capabilityChanges.push( | |
| clawAddCapabilityChange({ | |
| kind: "package", | |
| id: `${pkg.kind}:${pkg.ref}`, | |
| path: `packages.${pkg.kind}.${pkg.ref}`, | |
| action: preflight.ok && preflight.action === "reuse" ? "reuse" : "install", | |
| reason: | |
| preflight.ok && preflight.action === "reuse" | |
| ? "The Claw requires access to an existing package capability." | |
| : "The Claw requires downloadable package content or executable code.", | |
| effect: { | |
| kind: pkg.kind, | |
| source: pkg.source, | |
| ref: pkg.ref, | |
| version: pkg.version, | |
| integrity: preflight.integrity ?? "unresolved", | |
| ...(preflight.installId ? { installId: preflight.installId } : {}), | |
| ...(preflight.warning ? { riskWarning: preflight.warning } : {}), | |
| }, | |
| }), | |
| ); | |
| } | |
| const extensionPlan = await planClawExtensions({ | |
| extensions: params.openClawProfile?.extensions ?? [], | |
| workspace, | |
| packagePreflight: context.packagePreflight, | |
| }); | |
| const extensions = extensionPlan.extensions; | |
| const extensionCollisions = findClawExtensionPackageCollisions({ | |
| packages: params.manifest.packages, | |
| extensions: params.openClawProfile?.extensions ?? [], | |
| }); | |
| const collisionIndexes = new Set(extensionCollisions.map(({ index }) => index)); | |
| blockers.push(...extensionCollisions.map(({ diagnostic }) => diagnostic)); | |
| for (const [index, action] of extensionPlan.actions.entries()) { | |
| if (collisionIndexes.has(index)) { | |
| continue; | |
| } | |
| actions.push(action); | |
| } | |
| capabilityChanges.push(...extensionPlan.capabilityChanges); | |
| readinessRequirements.push(...extensionPlan.requirements); | |
| blockers.push(...extensionPlan.blockers); | |
| const existingMcpServerNames = new Set(context.existingMcpServerNames ?? []); | |
| for (const [name, server] of Object.entries(params.manifest.mcpServers)) { | |
| const existingServer = context.existingMcpServers?.[name]; | |
| const exactExisting = | |
| existingServer !== undefined && | |
| digestClawMcpServer(existingServer) === digestClawMcpServer(server); | |
| const blocked = | |
| !exactExisting && (existingMcpServerNames.has(name) || existingServer !== undefined); | |
| if (blocked) { | |
| blockers.push( | |
| blocker( | |
| "mcp_server_collision", | |
| `$.mcpServers.${name}`, | |
| `MCP server ${JSON.stringify(name)} already exists with different or unresolved configuration and will not be overwritten.`, | |
| ), | |
| ); | |
| } | |
| if ("env" in server) { | |
| for (const value of Object.values(server.env ?? {})) { | |
| readinessRequirements.push({ | |
| kind: "environment", | |
| mcpServer: name, | |
| name: value.slice(2, -1), | |
| }); | |
| } | |
| } | |
| if ("auth" in server && server.auth === "oauth") { | |
| readinessRequirements.push({ kind: "oauth", mcpServer: name }); | |
| } | |
| actions.push({ | |
| kind: "mcpServer", | |
| id: name, | |
| action: "configure", | |
| target: `mcp.servers.${name}`, | |
| details: { | |
| ...server, | |
| expectedState: exactExisting ? "present-exact" : "absent", | |
| prerequisites: readinessRequirements.filter( | |
| (requirement) => requirement.kind !== "plugin-setup" && requirement.mcpServer === name, | |
| ), | |
| }, | |
| blocked, | |
| }); | |
| capabilityChanges.push( | |
| clawAddCapabilityChange({ | |
| kind: "mcpServer", | |
| id: name, | |
| path: `mcpServers.${name}`, | |
| action: "configure", | |
| reason: "The Claw declares an MCP execution or network tool surface.", | |
| effect: { | |
| ...server, | |
| ...("env" in server && server.env ? { env: Object.keys(server.env).toSorted() } : {}), | |
| }, | |
| }), | |
| ); | |
| } | |
| // Strict v1 validation permits only deterministic main or isolated targets. | |
| for (const job of params.manifest.cronJobs) { | |
| actions.push({ | |
| kind: "cronJob", | |
| id: job.id, | |
| action: "schedule", | |
| target: `cron:${job.id}:agent=${finalId}`, | |
| details: { | |
| ...job, | |
| agentId: finalId, | |
| expectedState: "absent", | |
| ...(job.delivery?.channel === "last" | |
| ? { deliveryResolution: "local-channel-state:last" } | |
| : {}), | |
| }, | |
| blocked: false, | |
| }); | |
| capabilityChanges.push( | |
| clawAddCapabilityChange({ | |
| kind: "cronJob", | |
| id: job.id, | |
| path: `cronJobs.${job.id}`, | |
| action: "schedule", | |
| reason: "The Claw declares recurring scheduled work.", | |
| effect: { ...job, agentId: finalId }, | |
| }), | |
| ); | |
| } | |
| capabilityChanges.sort((left, right) => | |
| `${left.kind}:${left.id}:${left.path}`.localeCompare(`${right.kind}:${right.id}:${right.path}`), | |
| ); | |
| const notices = clawAgentConfigurationNotices( | |
| openClawAgentSettings, | |
| context.config ?? {}, | |
| new Set([...existingAgentIds, finalId]), | |
| ); | |
| const planIntegrity = `sha256:${createHash("sha256") | |
| .update( | |
| stableStringify({ | |
| manifestSchemaVersion: params.manifest.schemaVersion, | |
| clawIntegrity: source.integrity, | |
| finalId, | |
| workspace, | |
| actions, | |
| capabilityChanges, | |
| blockers, | |
| extensions, | |
| ...(notices.length > 0 ? { notices } : {}), | |
| }), | |
| ) | |
| .digest("hex")}`; | |
| return { | |
| schemaVersion: CLAW_ADD_PLAN_SCHEMA_VERSION, | |
| manifestSchemaVersion: params.manifest.schemaVersion, | |
| stability: CLAW_OUTPUT_STABILITY, | |
| dryRun: true, | |
| mutationAllowed: false, | |
| planIntegrity, | |
| claw: planSource, | |
| agent: { | |
| requestedId: params.manifest.agent.id, | |
| finalId, | |
| workspace, | |
| config: agentConfig, | |
| }, | |
| summary: { | |
| totalActions: actions.length, | |
| agentActions: actions.filter((action) => action.kind === "agent").length, | |
| workspaceActions: actions.filter( | |
| (action) => | |
| action.kind === "workspace" || | |
| action.kind === "bootstrap" || | |
| action.kind === "workspaceFile", | |
| ).length, | |
| packageActions: actions.filter((action) => action.kind === "package").length, | |
| mcpServerActions: actions.filter((action) => action.kind === "mcpServer").length, | |
| cronJobActions: actions.filter((action) => action.kind === "cronJob").length, | |
| blockedActions: actions.filter((action) => action.blocked).length, | |
| capabilityEscalations: capabilityChanges.length, | |
| }, | |
| actions, | |
| capabilityChanges, | |
| readiness: { | |
| ready: readinessRequirements.length === 0, | |
| requirements: readinessRequirements, | |
| }, | |
| extensions, | |
| blockers, | |
| diagnostics: [...(params.diagnostics ?? []), ...notices], | |
| }; | |
| } | |