mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 11:25:50 -06:00
Apply consented grouped Claw updates (#102982)
* Apply consented grouped Claw updates * test(claws): cover update application * docs(claws): document consented updates * fix(claws): preserve uncertain update state * fix(claws): preserve inherited lifecycle guards * fix(claws): derive update package contracts * fix(claws): type update package lookup * test(claws): complete update capability fixtures * refactor(claws): share cron payload builder for updates * refactor(claws): extract update plan summary * fix(claws): activate cron after update dependencies * fix(claws): preserve state after uncertain cron update * fix(claws): persist partial update provenance * fix(claws): preserve update ownership metadata * test(claws): track update apply temp dirs * test(claws): align update cron mock result * fix(claws): update apply writes agent entries --------- Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
This commit is contained in:
@@ -34,7 +34,6 @@ import {
|
||||
CLAW_OUTPUT_STABILITY,
|
||||
type ClawAddPlan,
|
||||
} from "../claws/types.js";
|
||||
import { buildClawUpdatePlan, CLAW_UPDATE_PLAN_SCHEMA_VERSION } from "../claws/update-plan.js";
|
||||
// Runtime handlers for experimental local Claws commands.
|
||||
import { getRuntimeConfig } from "../config/config.js";
|
||||
import { listConfiguredMcpServers } from "../config/mcp-config.js";
|
||||
@@ -45,15 +44,12 @@ import {
|
||||
} from "../cron/store.js";
|
||||
import { redactSensitiveText } from "../logging/redact.js";
|
||||
import { defaultRuntime, writeRuntimeJson, type RuntimeEnv } from "../runtime.js";
|
||||
import { openExistingOpenClawStateDatabaseReadOnly } from "../state/openclaw-state-db.js";
|
||||
import { logClawUpdatePlanSummary } from "./claws-cli-update-output.js";
|
||||
import type {
|
||||
ClawsAddOptions,
|
||||
ClawsExportOptions,
|
||||
ClawsInspectOptions,
|
||||
ClawsRemoveOptions,
|
||||
ClawsStatusOptions,
|
||||
ClawsUpdateOptions,
|
||||
} from "./claws-cli.js";
|
||||
import { callGatewayFromCli } from "./gateway-rpc.js";
|
||||
|
||||
@@ -406,162 +402,7 @@ export async function runClawsStatusCommand(
|
||||
}
|
||||
}
|
||||
|
||||
export async function runClawsUpdateCommand(
|
||||
target: string,
|
||||
opts: ClawsUpdateOptions,
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
): Promise<void> {
|
||||
assertExperimentalClawsEnabled();
|
||||
if (!opts.dryRun) {
|
||||
const message =
|
||||
"Claw update is read-only in this implementation slice; pass --dry-run to preview changes.";
|
||||
if (opts.json) {
|
||||
writeRuntimeJson(runtime, {
|
||||
schemaVersion: CLAW_UPDATE_PLAN_SCHEMA_VERSION,
|
||||
stability: CLAW_OUTPUT_STABILITY,
|
||||
ok: false,
|
||||
error: { code: "update_preview_required", message },
|
||||
});
|
||||
} else {
|
||||
runtime.error(message);
|
||||
}
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const listedMcpServers = await listConfiguredMcpServers();
|
||||
if (!listedMcpServers.ok) {
|
||||
if (opts.json) {
|
||||
writeRuntimeJson(runtime, {
|
||||
schemaVersion: CLAW_UPDATE_PLAN_SCHEMA_VERSION,
|
||||
stability: CLAW_OUTPUT_STABILITY,
|
||||
dryRun: true,
|
||||
mutationAllowed: false,
|
||||
valid: false,
|
||||
diagnostics: [
|
||||
{
|
||||
level: "error",
|
||||
code: "mcp_config_unavailable",
|
||||
phase: "plan",
|
||||
path: "$.mcpServers",
|
||||
message: listedMcpServers.error,
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
runtime.error(listedMcpServers.error);
|
||||
}
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
let source = opts.from;
|
||||
if (!source) {
|
||||
const database = openExistingOpenClawStateDatabaseReadOnly();
|
||||
let status: Awaited<ReturnType<typeof readClawStatus>> | { records: never[] } = {
|
||||
records: [],
|
||||
};
|
||||
if (database) {
|
||||
try {
|
||||
const hasClawInstalls =
|
||||
database.db /* sqlite-allow-raw: read-only Claw install table-existence probe. */
|
||||
.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'claw_installs'")
|
||||
.get();
|
||||
if (hasClawInstalls) {
|
||||
status = await readClawStatus(target, {
|
||||
database,
|
||||
readOnly: true,
|
||||
sourceMcpServers: listedMcpServers.mcpServers,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
database.walMaintenance.close();
|
||||
}
|
||||
}
|
||||
if (status.records.length !== 1) {
|
||||
const message =
|
||||
status.records.length === 0
|
||||
? `No installed Claw agent matches ${JSON.stringify(target)}.`
|
||||
: `Claw name ${JSON.stringify(target)} matches multiple agents; use an agent id.`;
|
||||
if (opts.json) {
|
||||
writeRuntimeJson(runtime, {
|
||||
schemaVersion: CLAW_UPDATE_PLAN_SCHEMA_VERSION,
|
||||
stability: CLAW_OUTPUT_STABILITY,
|
||||
dryRun: true,
|
||||
mutationAllowed: false,
|
||||
valid: false,
|
||||
diagnostics: [
|
||||
{
|
||||
level: "error",
|
||||
code: status.records.length === 0 ? "claw_not_found" : "claw_ambiguous",
|
||||
phase: "plan",
|
||||
path: "$",
|
||||
message,
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
runtime.error(message);
|
||||
}
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
const recorded = status.records[0]!.install.claw;
|
||||
source = recorded.kind === "package" ? recorded.packageRoot : recorded.manifestPath;
|
||||
}
|
||||
|
||||
const loaded = await readClawManifestFile(source);
|
||||
if (!loaded.ok) {
|
||||
const diagnostics = opts.from
|
||||
? loaded.diagnostics
|
||||
: [
|
||||
...loaded.diagnostics,
|
||||
{
|
||||
level: "error" as const,
|
||||
code: "recorded_source_unavailable",
|
||||
phase: "plan" as const,
|
||||
path: "$",
|
||||
message: "The recorded Claw source is unavailable; pass --from to override it.",
|
||||
},
|
||||
];
|
||||
if (opts.json) {
|
||||
writeRuntimeJson(runtime, {
|
||||
schemaVersion: CLAW_UPDATE_PLAN_SCHEMA_VERSION,
|
||||
stability: CLAW_OUTPUT_STABILITY,
|
||||
dryRun: true,
|
||||
mutationAllowed: false,
|
||||
valid: false,
|
||||
diagnostics,
|
||||
});
|
||||
} else {
|
||||
runtime.error(formatDiagnostics(diagnostics));
|
||||
}
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const plan = await buildClawUpdatePlan({
|
||||
agentId: target,
|
||||
targetManifest: loaded.manifest,
|
||||
targetSource: loaded.source,
|
||||
config: getRuntimeConfig(),
|
||||
sourceMcpServers: listedMcpServers.mcpServers,
|
||||
packagePreflight: preflightClawPackage,
|
||||
diagnostics: loaded.diagnostics,
|
||||
});
|
||||
if (opts.json) {
|
||||
writeRuntimeJson(runtime, plan);
|
||||
} else {
|
||||
logExperimentalWarning(runtime);
|
||||
runtime.log(
|
||||
`Claw update plan: ${plan.currentClaw?.name ?? target} ${plan.currentClaw?.version ?? "unknown"} -> ${plan.targetClaw?.version ?? "unknown"}`,
|
||||
);
|
||||
logClawUpdatePlanSummary(plan, runtime);
|
||||
}
|
||||
if (plan.blockers.length > 0 || plan.actions.some((action) => action.blocked)) {
|
||||
runtime.exit(1);
|
||||
}
|
||||
}
|
||||
export { runClawsUpdateCommand } from "./claws-update-cli.runtime.js";
|
||||
|
||||
export async function runClawsRemoveCommand(
|
||||
target: string,
|
||||
|
||||
+101
-2
@@ -34,6 +34,7 @@ const mocks = vi.hoisted(() => {
|
||||
readClawStatus: vi.fn(),
|
||||
buildClawRemovePlan: vi.fn(),
|
||||
applyClawRemovePlan: vi.fn(),
|
||||
applyClawUpdatePlan: vi.fn(),
|
||||
buildClawUpdatePlan: vi.fn(),
|
||||
exportClawAgent: vi.fn(),
|
||||
};
|
||||
@@ -88,8 +89,14 @@ vi.mock("../claws/update-plan.js", async () => ({
|
||||
buildClawUpdatePlan: mocks.buildClawUpdatePlan,
|
||||
}));
|
||||
|
||||
vi.mock("../claws/update-apply.js", async () => ({
|
||||
...(await vi.importActual<typeof import("../claws/update-apply.js")>("../claws/update-apply.js")),
|
||||
applyClawUpdatePlan: mocks.applyClawUpdatePlan,
|
||||
}));
|
||||
|
||||
const { registerClawsCli } = await import("./claws-cli.js");
|
||||
const { runClawsAddCommand } = await import("./claws-cli.runtime.js");
|
||||
const { ClawUpdateMutationError } = await import("../claws/update-apply.js");
|
||||
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
||||
|
||||
const minimalManifest = { schemaVersion: 1, agent: { id: "demo-agent", name: "Demo Agent" } };
|
||||
@@ -213,7 +220,7 @@ describe("claws cli", () => {
|
||||
kind: "agent",
|
||||
id: "demo-agent",
|
||||
action: "remove",
|
||||
target: "agents.list[demo-agent]",
|
||||
target: 'agents.entries["demo-agent"]',
|
||||
blocked: false,
|
||||
},
|
||||
],
|
||||
@@ -273,6 +280,19 @@ describe("claws cli", () => {
|
||||
blockers: [],
|
||||
diagnostics: [],
|
||||
});
|
||||
mocks.applyClawUpdatePlan.mockReset();
|
||||
mocks.applyClawUpdatePlan.mockResolvedValue({
|
||||
schemaVersion: "openclaw.clawUpdateResult.v1",
|
||||
stability: "experimental",
|
||||
dryRun: false,
|
||||
mutationAllowed: true,
|
||||
status: "complete",
|
||||
agentId: "demo-agent",
|
||||
previousClaw: { name: "@acme/demo-agent", version: "1.0.0", integrity: "sha256:old" },
|
||||
targetClaw: { name: "@acme/demo-agent", version: "1.2.3", integrity: "sha256:new" },
|
||||
appliedActions: [],
|
||||
installRecord: { agentId: "demo-agent" },
|
||||
});
|
||||
mocks.exportClawAgent.mockReset();
|
||||
mocks.exportClawAgent.mockResolvedValue({
|
||||
schemaVersion: "openclaw.clawExportResult.v1",
|
||||
@@ -849,7 +869,86 @@ describe("claws cli", () => {
|
||||
expect(mocks.buildClawUpdatePlan).not.toHaveBeenCalled();
|
||||
expect(JSON.parse(mocks.logs[0] ?? "{}")).toMatchObject({
|
||||
schemaVersion: "openclaw.clawUpdatePlan.v1",
|
||||
error: { code: "update_preview_required" },
|
||||
error: { code: "consent_required" },
|
||||
});
|
||||
expect(mocks.runtime.exit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it("requires exact plan integrity with update consent", async () => {
|
||||
const { root } = await writePackage();
|
||||
|
||||
await runCli(["claws", "update", "demo-agent", "--from", root, "--yes", "--json"]);
|
||||
|
||||
expect(mocks.buildClawUpdatePlan).not.toHaveBeenCalled();
|
||||
expect(JSON.parse(mocks.logs[0] ?? "{}")).toMatchObject({
|
||||
error: { code: "consent_required" },
|
||||
});
|
||||
expect(mocks.runtime.exit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it("applies a supported update only after explicit consent", async () => {
|
||||
const { root } = await writePackage();
|
||||
|
||||
await runCli([
|
||||
"claws",
|
||||
"update",
|
||||
"demo-agent",
|
||||
"--from",
|
||||
root,
|
||||
"--yes",
|
||||
"--plan-integrity",
|
||||
"sha256:update-plan",
|
||||
"--json",
|
||||
]);
|
||||
|
||||
expect(mocks.applyClawUpdatePlan).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ agentId: "demo-agent" }),
|
||||
expect.objectContaining({
|
||||
targetManifest: expect.objectContaining({
|
||||
agent: { id: "demo-agent", name: "Demo Agent" },
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
config: {},
|
||||
sourceMcpServers: {},
|
||||
consentPlanIntegrity: "sha256:update-plan",
|
||||
packagePreflight: expect.any(Function),
|
||||
cronGateway: expect.objectContaining({
|
||||
add: expect.any(Function),
|
||||
get: expect.any(Function),
|
||||
remove: expect.any(Function),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(JSON.parse(mocks.logs[0] ?? "{}")).toMatchObject({
|
||||
schemaVersion: "openclaw.clawUpdateResult.v1",
|
||||
status: "complete",
|
||||
agentId: "demo-agent",
|
||||
});
|
||||
});
|
||||
|
||||
it("reports uncertain update mutations as partial JSON", async () => {
|
||||
const { root } = await writePackage();
|
||||
mocks.applyClawUpdatePlan.mockRejectedValueOnce(
|
||||
new ClawUpdateMutationError("update_partial", "artifact outcome requires reconciliation"),
|
||||
);
|
||||
|
||||
await runCli([
|
||||
"claws",
|
||||
"update",
|
||||
"demo-agent",
|
||||
"--from",
|
||||
root,
|
||||
"--yes",
|
||||
"--plan-integrity",
|
||||
"sha256:update-plan",
|
||||
"--json",
|
||||
]);
|
||||
|
||||
expect(JSON.parse(mocks.logs[0] ?? "{}")).toMatchObject({
|
||||
schemaVersion: "openclaw.clawUpdateResult.v1",
|
||||
status: "partial",
|
||||
error: { code: "update_partial" },
|
||||
});
|
||||
expect(mocks.runtime.exit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
@@ -17,7 +17,13 @@ export type ClawsAddOptions = {
|
||||
};
|
||||
|
||||
export type ClawsStatusOptions = { json?: boolean };
|
||||
export type ClawsUpdateOptions = { from?: string; dryRun?: boolean; json?: boolean };
|
||||
export type ClawsUpdateOptions = {
|
||||
from?: string;
|
||||
dryRun?: boolean;
|
||||
yes?: boolean;
|
||||
planIntegrity?: string;
|
||||
json?: boolean;
|
||||
};
|
||||
export type ClawsRemoveOptions = {
|
||||
dryRun?: boolean;
|
||||
yes?: boolean;
|
||||
@@ -80,6 +86,8 @@ export function registerClawsCli(program: Command) {
|
||||
.argument("<claw-or-agent>", "Installed package name or final agent id")
|
||||
.option("--from <source>", "Override the target source recorded at Claw add time")
|
||||
.option("--dry-run", "Preview update actions without mutating state", false)
|
||||
.option("--yes", "Confirm the exact supported update plan", false)
|
||||
.option("--plan-integrity <digest>", "Bind consent to an exact update plan")
|
||||
.option("--json", "Print JSON", false)
|
||||
.action(async (target: string, opts: ClawsUpdateOptions) => {
|
||||
const { runClawsUpdateCommand } = await import("./claws-cli.runtime.js");
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
import { assertExperimentalClawsEnabled } from "../claws/experimental.js";
|
||||
import { readClawStatus } from "../claws/lifecycle-state.js";
|
||||
import { preflightClawPackage } from "../claws/packages.js";
|
||||
import { readClawManifestFile } from "../claws/reader.js";
|
||||
import { CLAW_OUTPUT_STABILITY } from "../claws/types.js";
|
||||
import {
|
||||
applyClawUpdatePlan,
|
||||
CLAW_UPDATE_RESULT_SCHEMA_VERSION,
|
||||
ClawUpdateMutationError,
|
||||
} from "../claws/update-apply.js";
|
||||
import { buildClawUpdatePlan, CLAW_UPDATE_PLAN_SCHEMA_VERSION } from "../claws/update-plan.js";
|
||||
import { listConfiguredMcpServers } from "../config/mcp-config.js";
|
||||
import { defaultRuntime, writeRuntimeJson, type RuntimeEnv } from "../runtime.js";
|
||||
import { openExistingOpenClawStateDatabaseReadOnly } from "../state/openclaw-state-db.js";
|
||||
import { logClawUpdatePlanSummary } from "./claws-cli-update-output.js";
|
||||
import type { ClawsUpdateOptions } from "./claws-cli.js";
|
||||
import { callGatewayFromCli } from "./gateway-rpc.js";
|
||||
|
||||
type DiagnosticLike = { level: string; code: string; path: string; message: string };
|
||||
|
||||
function formatDiagnostics(diagnostics: DiagnosticLike[]): string {
|
||||
return diagnostics
|
||||
.map(
|
||||
(diagnostic) =>
|
||||
`${diagnostic.level.toUpperCase()} ${diagnostic.code} ${diagnostic.path}: ${diagnostic.message}`,
|
||||
)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function logExperimentalWarning(runtime: RuntimeEnv): void {
|
||||
runtime.log("Experimental: Claws contracts may change while RFC 0016 is under review.");
|
||||
}
|
||||
|
||||
export async function runClawsUpdateCommand(
|
||||
target: string,
|
||||
opts: ClawsUpdateOptions,
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
): Promise<void> {
|
||||
assertExperimentalClawsEnabled();
|
||||
if (!opts.dryRun && (!opts.yes || !opts.planIntegrity)) {
|
||||
const message =
|
||||
"Claw update requires explicit consent; pass --dry-run to preview or --yes with --plan-integrity to apply supported actions.";
|
||||
if (opts.json) {
|
||||
writeRuntimeJson(runtime, {
|
||||
schemaVersion: CLAW_UPDATE_PLAN_SCHEMA_VERSION,
|
||||
stability: CLAW_OUTPUT_STABILITY,
|
||||
ok: false,
|
||||
error: { code: "consent_required", message },
|
||||
});
|
||||
} else {
|
||||
runtime.error(message);
|
||||
}
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const listedMcpServers = await listConfiguredMcpServers();
|
||||
if (!listedMcpServers.ok) {
|
||||
if (opts.json) {
|
||||
writeRuntimeJson(runtime, {
|
||||
schemaVersion: CLAW_UPDATE_PLAN_SCHEMA_VERSION,
|
||||
stability: CLAW_OUTPUT_STABILITY,
|
||||
dryRun: true,
|
||||
mutationAllowed: false,
|
||||
valid: false,
|
||||
diagnostics: [
|
||||
{
|
||||
level: "error",
|
||||
code: "mcp_config_unavailable",
|
||||
phase: "plan",
|
||||
path: "$.mcpServers",
|
||||
message: listedMcpServers.error,
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
runtime.error(listedMcpServers.error);
|
||||
}
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
const config = listedMcpServers.config;
|
||||
|
||||
let source = opts.from;
|
||||
if (!source) {
|
||||
const database = openExistingOpenClawStateDatabaseReadOnly();
|
||||
let status: Awaited<ReturnType<typeof readClawStatus>> | { records: never[] } = {
|
||||
records: [],
|
||||
};
|
||||
if (database) {
|
||||
try {
|
||||
const hasClawInstalls =
|
||||
database.db /* sqlite-allow-raw: read-only Claw install table-existence probe. */
|
||||
.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'claw_installs'")
|
||||
.get();
|
||||
if (hasClawInstalls) {
|
||||
status = await readClawStatus(target, {
|
||||
database,
|
||||
readOnly: true,
|
||||
sourceMcpServers: listedMcpServers.mcpServers,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
database.walMaintenance.close();
|
||||
}
|
||||
}
|
||||
if (status.records.length !== 1) {
|
||||
const message =
|
||||
status.records.length === 0
|
||||
? `No installed Claw agent matches ${JSON.stringify(target)}.`
|
||||
: `Claw name ${JSON.stringify(target)} matches multiple agents; use an agent id.`;
|
||||
if (opts.json) {
|
||||
writeRuntimeJson(runtime, {
|
||||
schemaVersion: CLAW_UPDATE_PLAN_SCHEMA_VERSION,
|
||||
stability: CLAW_OUTPUT_STABILITY,
|
||||
dryRun: true,
|
||||
mutationAllowed: false,
|
||||
valid: false,
|
||||
diagnostics: [
|
||||
{
|
||||
level: "error",
|
||||
code: status.records.length === 0 ? "claw_not_found" : "claw_ambiguous",
|
||||
phase: "plan",
|
||||
path: "$",
|
||||
message,
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
runtime.error(message);
|
||||
}
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
const recorded = status.records[0]!.install.claw;
|
||||
source = recorded.kind === "package" ? recorded.packageRoot : recorded.manifestPath;
|
||||
}
|
||||
|
||||
const loaded = await readClawManifestFile(source);
|
||||
if (!loaded.ok) {
|
||||
const diagnostics = opts.from
|
||||
? loaded.diagnostics
|
||||
: [
|
||||
...loaded.diagnostics,
|
||||
{
|
||||
level: "error" as const,
|
||||
code: "recorded_source_unavailable",
|
||||
phase: "plan" as const,
|
||||
path: "$",
|
||||
message: "The recorded Claw source is unavailable; pass --from to override it.",
|
||||
},
|
||||
];
|
||||
if (opts.json) {
|
||||
writeRuntimeJson(runtime, {
|
||||
schemaVersion: CLAW_UPDATE_PLAN_SCHEMA_VERSION,
|
||||
stability: CLAW_OUTPUT_STABILITY,
|
||||
dryRun: true,
|
||||
mutationAllowed: false,
|
||||
valid: false,
|
||||
diagnostics,
|
||||
});
|
||||
} else {
|
||||
runtime.error(formatDiagnostics(diagnostics));
|
||||
}
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const plan = await buildClawUpdatePlan({
|
||||
agentId: target,
|
||||
targetManifest: loaded.manifest,
|
||||
targetSource: loaded.source,
|
||||
config,
|
||||
sourceMcpServers: listedMcpServers.mcpServers,
|
||||
packagePreflight: preflightClawPackage,
|
||||
diagnostics: loaded.diagnostics,
|
||||
});
|
||||
if (opts.dryRun || plan.blockers.length > 0 || plan.actions.some((action) => action.blocked)) {
|
||||
if (opts.json) {
|
||||
writeRuntimeJson(runtime, plan);
|
||||
} else {
|
||||
logExperimentalWarning(runtime);
|
||||
runtime.log(
|
||||
`Claw update plan: ${plan.currentClaw?.name ?? target} ${plan.currentClaw?.version ?? "unknown"} -> ${plan.targetClaw?.version ?? "unknown"}`,
|
||||
);
|
||||
runtime.log(`Plan integrity: ${plan.planIntegrity}`);
|
||||
logClawUpdatePlanSummary(plan, runtime);
|
||||
}
|
||||
if (plan.blockers.length > 0 || plan.actions.some((action) => action.blocked)) {
|
||||
runtime.exit(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await applyClawUpdatePlan(
|
||||
plan,
|
||||
{ targetManifest: loaded.manifest, targetSource: loaded.source },
|
||||
{
|
||||
config,
|
||||
sourceMcpServers: listedMcpServers.mcpServers,
|
||||
consentPlanIntegrity: opts.planIntegrity,
|
||||
packagePreflight: preflightClawPackage,
|
||||
cronGateway: {
|
||||
add: async (input) => await callGatewayFromCli("cron.add", {}, input),
|
||||
get: async (id) => await callGatewayFromCli("cron.get", {}, { id }),
|
||||
remove: async (id) => await callGatewayFromCli("cron.remove", {}, { id }),
|
||||
},
|
||||
},
|
||||
);
|
||||
if (opts.json) {
|
||||
writeRuntimeJson(runtime, result);
|
||||
return;
|
||||
}
|
||||
logExperimentalWarning(runtime);
|
||||
runtime.log(`Updated agent: ${result.agentId}`);
|
||||
runtime.log(`Claw version: ${result.previousClaw.version} -> ${result.targetClaw.version}`);
|
||||
} catch (error) {
|
||||
const code = error instanceof ClawUpdateMutationError ? error.code : "update_failed";
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (opts.json) {
|
||||
writeRuntimeJson(runtime, {
|
||||
schemaVersion: CLAW_UPDATE_RESULT_SCHEMA_VERSION,
|
||||
stability: CLAW_OUTPUT_STABILITY,
|
||||
status: code === "update_partial" ? "partial" : "failed",
|
||||
error: { code, message },
|
||||
});
|
||||
} else {
|
||||
runtime.error(message);
|
||||
}
|
||||
runtime.exit(1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user