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:
Gio Della-Libera
2026-07-22 17:42:42 -07:00
committed by GitHub
parent 1a7a1808f7
commit 7d159fdc77
31 changed files with 4274 additions and 245 deletions
+1 -160
View File
@@ -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,