Files
openclaw/src/claws/packages.ts
T
Peter Steinberger b080dd1e76 refactor: consolidate coercion contracts (#122458)
* refactor: consolidate coercion contracts

Centralize exact string, record, numeric, date, Boolean, argument, and structured-error coercions while preserving call-site semantics.

Migrate canonical-name collisions and deprecated internal SDK bypasses, deleting 55 net production/tooling lines. Expand declaration ownership enforcement to 101 allowed helpers and add a narrow export-completeness audit.

* fix: preserve standalone script coercions

Keep copied Control UI tooling self-contained and retain the trusted release harness module-relative source seam when the harness runs against an old target cwd.
2026-08-11 23:26:37 -07:00

711 lines
26 KiB
TypeScript

import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { coerceErrorMessage, stableStringify } from "@openclaw/normalization-core";
import { runPluginInstallCommand } from "../cli/plugins-install-command.js";
import { runPluginUninstallCommand } from "../cli/plugins-uninstall-command.js";
import { normalizeClawHubSha256Integrity } from "../infra/clawhub-artifacts.js";
import { installPluginFromClawHub } from "../plugins/clawhub.js";
import { PLUGIN_ARTIFACT_ADAPTER_IDENTITY } from "../plugins/install-artifact-inspection.js";
import {
preflightPluginInstall,
resolveInstalledClawHubPlugin,
} from "../plugins/plugin-install-preflight.js";
import { withPluginLifecycleLease } from "../plugins/plugin-lifecycle-lease.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import { installSkillFromClawHub, preflightSkillFromClawHub } from "../skills/lifecycle/clawhub.js";
import {
acquireClawPackageLifecycleLease,
maintainClawPackageLifecycleLease,
type MaintainedClawPackageLifecycleLease,
} from "../state/claw-package-lifecycle-lease.js";
import type { OpenClawStateDatabaseOptions } from "../state/openclaw-state-db.js";
import { findResumableIntroducedPluginRequirement } from "./package-resume.js";
import { resolveClawPluginSetupRequirements } from "./package-setup-requirements.js";
import {
persistClawPackageRef,
readClawPackageRefs,
updateClawPackageRefStatus,
type PersistedClawPackageRef,
} from "./provenance.js";
import type {
ClawAddPlan,
ClawAddPlanAction,
ClawPackage,
ClawPackagePreflightResult,
ResolvedClawPackage,
} from "./types.js";
export class ClawPackageInstallError extends Error {
constructor(
readonly code: string,
message: string,
readonly installedPackages: PersistedClawPackageRef[],
) {
super(message);
this.name = "ClawPackageInstallError";
}
}
type PackageInstallerDeps = {
installPlugin?: typeof runPluginInstallCommand;
uninstallPlugin?: typeof runPluginUninstallCommand;
probePlugin?: typeof installPluginFromClawHub;
installSkill?: typeof installSkillFromClawHub;
preflightPlugin?: typeof preflightPluginInstall;
preflightSkill?: typeof preflightSkillFromClawHub;
persistPackageRef?: typeof persistClawPackageRef;
completePackageRef?: typeof updateClawPackageRefStatus;
readPackageRefs?: typeof readClawPackageRefs;
acquirePackageLease?: typeof acquireClawPackageLifecycleLease;
resolvePlugin?: typeof resolveInstalledClawHubPlugin;
};
type PlannedClawPackage = ResolvedClawPackage & {
ownerAction: "install" | "reuse";
installId?: string;
riskWarning?: string;
};
function packageFromAction(action: ClawAddPlanAction): PlannedClawPackage {
const details = action.details as
| (Partial<ResolvedClawPackage> & {
ownerAction?: "install" | "reuse";
installId?: string;
riskWarning?: string;
})
| undefined;
if (details?.kind !== "skill" && details?.kind !== "plugin") {
throw new Error(`Package action ${JSON.stringify(action.id)} has no valid package kind.`);
}
if (
details.source !== "clawhub" ||
!details.ref ||
!details.version ||
!details.integrity ||
!normalizeClawHubSha256Integrity(details.integrity)
) {
throw new Error(
`Package action ${JSON.stringify(action.id)} is not a pinned ClawHub package with integrity.`,
);
}
if (details.ownerAction !== "install" && details.ownerAction !== "reuse") {
throw new Error(`Package action ${JSON.stringify(action.id)} has no planned owner state.`);
}
if (details.kind === "plugin" && !details.installId) {
throw new Error(`Package action ${JSON.stringify(action.id)} has no resolved plugin id.`);
}
return {
kind: details.kind,
source: details.source,
ref: details.ref,
version: details.version,
integrity: details.integrity,
ownerAction: details.ownerAction,
...(details.extension ? { extension: details.extension } : {}),
...(details.installId ? { installId: details.installId } : {}),
...(details.riskWarning ? { riskWarning: details.riskWarning } : {}),
};
}
function installerRuntime(runtime: RuntimeEnv): RuntimeEnv {
return {
log: (value) => runtime.log(value),
error: (value) => runtime.error(value),
exit: (code) => {
throw new Error(`Plugin installer exited with code ${code}.`);
},
};
}
function ownerInstallIsNewerThanRefs(
installedAt: string | undefined,
refs: PersistedClawPackageRef[],
): boolean {
const timestamp = Date.parse(installedAt ?? "");
return (
Number.isFinite(timestamp) &&
refs.length > 0 &&
refs.every((candidate) => timestamp > candidate.updatedAtMs)
);
}
type ClawPluginProbeDeps = {
probePlugin?: typeof installPluginFromClawHub;
createProbeExtensionsDir?: () => Promise<string>;
removeProbeExtensionsDir?: (path: string) => Promise<void>;
};
async function probeClawPluginArtifact(
pkg: ClawPackage,
isolateFromLiveExtensions: boolean,
deps: ClawPluginProbeDeps,
): Promise<Awaited<ReturnType<typeof installPluginFromClawHub>>> {
const probePlugin = deps.probePlugin ?? installPluginFromClawHub;
const request = {
spec: `clawhub:${pkg.ref}@${pkg.version}`,
dryRun: true,
acknowledgeClawHubRisk: true,
} as const;
if (!isolateFromLiveExtensions) {
return await probePlugin(request);
}
const probeExtensionsDir = await (
deps.createProbeExtensionsDir ??
(async () => await mkdtemp(join(tmpdir(), "openclaw-claw-plugin-probe-")))
)();
try {
return await probePlugin({ ...request, extensionsDir: probeExtensionsDir });
} finally {
try {
await (
deps.removeProbeExtensionsDir ??
(async (path: string) => await rm(path, { recursive: true, force: true }))
)(probeExtensionsDir);
} catch {
// Temporary probe cleanup must not replace the canonical preflight result.
}
}
}
export async function preflightClawPackage(
pkg: ClawPackage,
workspaceDir: string,
options: {
env?: NodeJS.ProcessEnv;
deps?: Pick<PackageInstallerDeps, "preflightPlugin"> & ClawPluginProbeDeps;
} = {},
): Promise<ClawPackagePreflightResult> {
if (pkg.kind === "skill") {
const result = await preflightSkillFromClawHub({
workspaceDir,
slug: pkg.ref,
version: pkg.version,
acknowledgeClawHubRisk: true,
});
return result.ok ? result : { ok: false, code: result.code, message: result.error };
}
const result = await (options.deps?.preflightPlugin ?? preflightPluginInstall)({
clawhubPackage: pkg.ref,
rawSpec: `clawhub:${pkg.ref}@${pkg.version}`,
expectedVersion: pkg.version,
});
if (!result.ok && result.code !== "plugin_version_conflict") {
return {
ok: false,
code: result.code,
message: result.error,
};
}
const probe = await probeClawPluginArtifact(
pkg,
!(result.ok && result.action === "install"),
options.deps ?? {},
);
if (!probe.ok) {
return { ok: false, code: probe.code ?? "plugin_preflight_failed", message: probe.error };
}
if (!probe.artifactInspection) {
return {
ok: false,
code: "plugin_artifact_inspection_unavailable",
message: `Plugin ${pkg.ref}@${pkg.version} did not return canonical artifact inspection.`,
};
}
if (probe.artifactInspection.format === "agent") {
return {
ok: false,
code: "plugin_artifact_format_unsupported",
message: `Plugin ${pkg.ref}@${pkg.version} uses unsupported Claw extension format agent.`,
};
}
const integrity = probe.clawhub.integrity
? normalizeClawHubSha256Integrity(probe.clawhub.integrity)
: null;
if (!integrity) {
return {
ok: false,
code: "plugin_integrity_unavailable",
message: `Plugin ${pkg.ref}@${pkg.version} did not resolve an artifact integrity.`,
};
}
const requirements = resolveClawPluginSetupRequirements({
pluginId: probe.pluginId,
setup: probe.setup,
env: options.env ?? process.env,
});
if (!result.ok) {
return {
ok: false,
code: result.code,
installedVersion: result.installedVersion,
integrity,
installId: probe.pluginId,
...(requirements.length > 0 ? { requirements } : {}),
detectedFormat: probe.artifactInspection.format,
mapped: probe.artifactInspection.mapped,
unavailable: probe.artifactInspection.unavailable,
adapterIdentity: PLUGIN_ARTIFACT_ADAPTER_IDENTITY,
...(probe.warning ? { warning: probe.warning } : {}),
message: `Plugin ${pkg.ref}@${pkg.version} conflicts with installed version ${result.installedVersion}.`,
};
}
if (
result.action === "reuse" &&
(result.installedId !== probe.pluginId ||
!result.installedIntegrity ||
normalizeClawHubSha256Integrity(result.installedIntegrity) !== integrity)
) {
return {
ok: false,
code: "plugin_integrity_conflict",
message: `Plugin ${pkg.ref}@${pkg.version} is installed as ${result.installedId} with integrity ${result.installedIntegrity ?? "unknown"}, expected ${probe.pluginId} with ${integrity}.`,
};
}
return {
ok: true,
action: result.action,
integrity,
installId: probe.pluginId,
...(result.action === "reuse" && result.installedIntegrity
? { installedIntegrity: result.installedIntegrity }
: {}),
...(result.action === "reuse" && result.installedAt ? { installedAt: result.installedAt } : {}),
...(requirements.length > 0 ? { requirements } : {}),
detectedFormat: probe.artifactInspection.format,
mapped: probe.artifactInspection.mapped,
unavailable: probe.artifactInspection.unavailable,
adapterIdentity: PLUGIN_ARTIFACT_ADAPTER_IDENTITY,
...(probe.warning ? { warning: probe.warning } : {}),
};
}
type InstallClawPackagesOptions = OpenClawStateDatabaseOptions & {
deps?: PackageInstallerDeps;
runtime?: RuntimeEnv;
nowMs?: number;
onExternalMutation?: (pkg: ClawPackage) => void;
};
export async function installClawPackages(
plan: ClawAddPlan,
options: InstallClawPackagesOptions = {},
): Promise<PersistedClawPackageRef[]> {
const includesPlugin = plan.actions.some(
(action) => action.kind === "package" && action.details?.kind === "plugin",
);
if (!includesPlugin) {
return await installClawPackagesUnlocked(plan, options);
}
return await withPluginLifecycleLease(
{
...(options.env ? { env: options.env } : {}),
...(options.path ? { path: options.path } : {}),
...(options.database ? { database: options.database } : {}),
},
async () => await installClawPackagesUnlocked(plan, options),
);
}
async function installClawPackagesUnlocked(
plan: ClawAddPlan,
options: InstallClawPackagesOptions,
): Promise<PersistedClawPackageRef[]> {
const deps = options.deps ?? {};
const installPlugin = deps.installPlugin ?? runPluginInstallCommand;
const uninstallPlugin = deps.uninstallPlugin ?? runPluginUninstallCommand;
const probePlugin = deps.probePlugin ?? installPluginFromClawHub;
const installSkill = deps.installSkill ?? installSkillFromClawHub;
const preflightPlugin = deps.preflightPlugin ?? preflightPluginInstall;
const preflightSkill = deps.preflightSkill ?? preflightSkillFromClawHub;
const persistPackageRef = deps.persistPackageRef ?? persistClawPackageRef;
const completePackageRef = deps.completePackageRef ?? updateClawPackageRefStatus;
const readPackageRefs = deps.readPackageRefs ?? readClawPackageRefs;
const acquirePackageLease = deps.acquirePackageLease ?? acquireClawPackageLifecycleLease;
const resolvePlugin = deps.resolvePlugin ?? resolveInstalledClawHubPlugin;
const runtime = options.runtime ?? defaultRuntime;
const installedPackages: PersistedClawPackageRef[] = [];
const installedPlugins: Array<{ installId: string; packageIndex: number }> = [];
for (const action of plan.actions.filter((candidate) => candidate.kind === "package")) {
let packageLease: MaintainedClawPackageLifecycleLease | null = null;
try {
const pkg = packageFromAction(action);
const leaseArtifact =
pkg.kind === "skill"
? {
kind: pkg.kind,
source: pkg.source,
ref: pkg.ref,
workspace: plan.agent.workspace,
}
: { kind: pkg.kind, source: pkg.source, ref: pkg.ref };
const acquiredLease = acquirePackageLease(leaseArtifact, {
env: options.env,
path: options.path,
required: true,
});
if (!acquiredLease) {
throw new Error(`Could not acquire package lifecycle lease for ${pkg.ref}.`);
}
packageLease = maintainClawPackageLifecycleLease(acquiredLease);
if (pkg.kind === "skill") {
const preflight = await preflightSkill({
workspaceDir: plan.agent.workspace,
slug: pkg.ref,
version: pkg.version,
expectedIntegrity: pkg.integrity,
acknowledgeClawHubRisk: true,
});
packageLease.assertCurrent();
if (!preflight.ok) {
throw new Error(preflight.error);
}
if (
preflight.action !== pkg.ownerAction ||
preflight.warning !== pkg.riskWarning ||
normalizeClawHubSha256Integrity(preflight.integrity) !==
normalizeClawHubSha256Integrity(pkg.integrity)
) {
throw new ClawPackageInstallError(
"package_owner_state_changed",
`Skill ${pkg.ref}@${pkg.version} changed after planning; run add --dry-run again.`,
installedPackages,
);
}
if (preflight.action === "reuse") {
installedPackages.push(
persistPackageRef(plan, pkg, {
...options,
status: "complete",
relationship: "managed",
origin: "pre-existing",
independentOwner: true,
}),
);
continue;
}
let packageRef = persistPackageRef(plan, pkg, {
...options,
status: "pending",
relationship: "managed",
origin: "claw-introduced",
independentOwner: false,
});
installedPackages.push(packageRef);
// The installer has no mutation receipt. Mark the boundary before calling it so a throw
// after an on-disk change is treated as uncertain instead of falsely reported as rolled back.
options.onExternalMutation?.(pkg);
const installed = await installSkill({
workspaceDir: plan.agent.workspace,
slug: pkg.ref,
version: pkg.version,
expectedIntegrity: pkg.integrity,
acknowledgeClawHubRisk: true,
clawManaged: true,
});
packageLease.assertCurrent();
if (!installed.ok) {
throw new Error(installed.error);
}
packageRef = completePackageRef(packageRef, "complete", options);
installedPackages[installedPackages.length - 1] = packageRef;
continue;
}
const preflight = await preflightPlugin({
clawhubPackage: pkg.ref,
rawSpec: `clawhub:${pkg.ref}@${pkg.version}`,
expectedVersion: pkg.version,
});
packageLease.assertCurrent();
if (!preflight.ok) {
throw new Error(
preflight.code === "plugin_version_conflict"
? `Plugin ${pkg.ref}@${pkg.version} conflicts with installed version ${preflight.installedVersion}.`
: preflight.error,
);
}
const resumableRequirement =
pkg.ownerAction === "install" && preflight.action === "reuse"
? findResumableIntroducedPluginRequirement({
agentId: plan.agent.finalId,
pkg,
preflight,
expectedIntegrity: pkg.integrity,
refs: readPackageRefs({
...options,
agentId: plan.agent.finalId,
kind: pkg.kind,
source: pkg.source,
ref: pkg.ref,
version: pkg.version,
}),
})
: undefined;
if (preflight.action !== pkg.ownerAction && !resumableRequirement) {
throw new ClawPackageInstallError(
"package_owner_state_changed",
`Plugin ${pkg.ref}@${pkg.version} owner state changed from ${pkg.ownerAction} to ${preflight.action}; run add --dry-run again.`,
installedPackages,
);
}
const probe = await probeClawPluginArtifact(pkg, preflight.action === "reuse", {
probePlugin,
});
packageLease.assertCurrent();
if (!probe.ok) {
throw new Error(probe.error);
}
const probeIntegrity = probe.clawhub.integrity
? normalizeClawHubSha256Integrity(probe.clawhub.integrity)
: null;
const plannedExtensionInspection = pkg.extension
? {
detectedFormat: pkg.extension.detectedFormat,
mapped: pkg.extension.mapped,
unavailable: pkg.extension.unavailable,
adapterIdentity: pkg.extension.adapterIdentity,
}
: undefined;
const probedExtensionInspection = probe.artifactInspection
? {
detectedFormat: probe.artifactInspection.format,
mapped: probe.artifactInspection.mapped,
unavailable: probe.artifactInspection.unavailable,
adapterIdentity: PLUGIN_ARTIFACT_ADAPTER_IDENTITY,
}
: undefined;
if (
probe.pluginId !== pkg.installId ||
probeIntegrity !== normalizeClawHubSha256Integrity(pkg.integrity) ||
probe.warning !== pkg.riskWarning ||
(plannedExtensionInspection &&
stableStringify(probedExtensionInspection) !==
stableStringify(plannedExtensionInspection))
) {
throw new ClawPackageInstallError(
"package_owner_state_changed",
`Plugin ${pkg.ref}@${pkg.version} identity or trust state changed after planning; run add --dry-run again.`,
installedPackages,
);
}
if (!pkg.installId) {
throw new ClawPackageInstallError(
"plugin_identity_unresolved",
`Plugin ${pkg.ref}@${pkg.version} has no resolved install identity.`,
installedPackages,
);
}
if (preflight.action === "reuse") {
if (
preflight.installedId !== pkg.installId ||
!preflight.installedIntegrity ||
normalizeClawHubSha256Integrity(preflight.installedIntegrity) !==
normalizeClawHubSha256Integrity(pkg.integrity)
) {
throw new ClawPackageInstallError(
"package_owner_state_changed",
`Plugin ${pkg.ref}@${pkg.version} identity changed after planning; run add --dry-run again.`,
installedPackages,
);
}
if (resumableRequirement) {
installedPackages.push(
persistPackageRef(plan, pkg, {
...options,
status: "complete",
relationship: resumableRequirement.relationship,
origin: resumableRequirement.origin,
independentOwner: resumableRequirement.independentOwner,
}),
);
continue;
}
const existingRefs = readPackageRefs({
...options,
kind: pkg.kind,
source: pkg.source,
ref: pkg.ref,
version: pkg.version,
});
const inheritsClawOrigin =
existingRefs.length > 0 &&
existingRefs.every(
(candidate) => candidate.origin === "claw-introduced" && !candidate.independentOwner,
) &&
!ownerInstallIsNewerThanRefs(preflight.installedAt, existingRefs);
installedPackages.push(
persistPackageRef(plan, pkg, {
...options,
status: "complete",
relationship: "referenced",
origin: inheritsClawOrigin ? "claw-introduced" : "pre-existing",
independentOwner: !inheritsClawOrigin,
}),
);
continue;
}
let packageRef = persistPackageRef(plan, pkg, {
...options,
status: "pending",
relationship: "referenced",
origin: "claw-introduced",
independentOwner: false,
});
installedPackages.push(packageRef);
// The installer has no mutation receipt. Mark the boundary before calling it so a throw
// after an on-disk change is treated as uncertain instead of falsely reported as rolled back.
options.onExternalMutation?.(pkg);
await installPlugin({
raw: `clawhub:${pkg.ref}@${pkg.version}`,
opts: {
acknowledgeClawHubRisk: true,
expectedIntegrity: pkg.integrity,
expectedPluginId: pkg.installId,
},
invalidateRuntimeCache: false,
clawManaged: true,
runtime: installerRuntime(runtime),
});
installedPlugins.push({
installId: pkg.installId,
packageIndex: installedPackages.length - 1,
});
packageLease.assertCurrent();
packageRef = completePackageRef(packageRef, "complete", options);
installedPackages[installedPackages.length - 1] = packageRef;
} catch (error) {
try {
packageLease?.release();
packageLease = null;
} catch {
// The rollback path will report a busy lease instead of mutating without ownership.
}
const pending = installedPackages.at(-1);
if (pending?.status === "pending") {
try {
installedPackages[installedPackages.length - 1] = completePackageRef(
pending,
"failed",
options,
);
} catch {
// Preserve the installer error; pending provenance still exposes uncertain ownership.
}
}
const rollbackErrors: string[] = [];
for (const installedPlugin of installedPlugins.toReversed()) {
const packageRef = installedPackages[installedPlugin.packageIndex];
if (!packageRef) {
continue;
}
let rollbackLease: MaintainedClawPackageLifecycleLease | null = null;
try {
const acquiredRollbackLease = acquirePackageLease(
{ kind: "plugin", source: "clawhub", ref: packageRef.ref },
{ env: options.env, path: options.path, required: true },
);
if (!acquiredRollbackLease) {
throw new Error(`Could not acquire package lifecycle lease for ${packageRef.ref}.`, {
cause: error,
});
}
rollbackLease = maintainClawPackageLifecycleLease(acquiredRollbackLease);
const sharedRefs = readPackageRefs({
...options,
kind: "plugin",
source: "clawhub",
ref: packageRef.ref,
version: packageRef.version,
integrity: packageRef.integrity,
}).filter(
(ref) =>
ref.agentId !== plan.agent.finalId &&
(ref.status === "pending" || ref.status === "complete"),
);
if (sharedRefs.length > 0) {
rollbackErrors.push(
`kept plugin ${installedPlugin.installId} because another Claw now references it`,
);
continue;
}
const currentRefs = readPackageRefs({
...options,
kind: "plugin",
source: "clawhub",
ref: packageRef.ref,
version: packageRef.version,
});
if (currentRefs.some((candidate) => candidate.independentOwner)) {
rollbackErrors.push(
`kept plugin ${installedPlugin.installId} because it now has a direct owner`,
);
continue;
}
const installed = await resolvePlugin({ clawhubPackage: packageRef.ref });
const installedIntegrity =
installed.status === "found" && installed.record.integrity
? normalizeClawHubSha256Integrity(installed.record.integrity)
: null;
if (
installed.status !== "found" ||
installed.pluginId !== installedPlugin.installId ||
installed.installedVersion !== packageRef.version ||
installedIntegrity !== normalizeClawHubSha256Integrity(packageRef.integrity) ||
ownerInstallIsNewerThanRefs(installed.record.installedAt, currentRefs)
) {
rollbackErrors.push(
`kept plugin ${installedPlugin.installId} because its installed identity changed after Claw installation`,
);
continue;
}
await uninstallPlugin(
installedPlugin.installId,
{ force: true, invalidateRuntimeCache: false, clawManaged: true },
installerRuntime(runtime),
);
rollbackLease.assertCurrent();
installedPackages[installedPlugin.packageIndex] = completePackageRef(
installedPackages[installedPlugin.packageIndex] ?? packageRef,
"rolled_back",
options,
);
} catch (rollbackError) {
rollbackErrors.push(
`could not remove plugin ${installedPlugin.installId}: ${coerceErrorMessage(rollbackError)}`,
);
continue;
} finally {
try {
rollbackLease?.release();
} catch {
// Lease expiry recovers cleanup when the shared state database is unavailable.
}
}
}
const message = coerceErrorMessage(error);
if (rollbackErrors.length > 0) {
throw new ClawPackageInstallError(
"package_rollback_failed",
`${message} Rollback incomplete: ${rollbackErrors.join("; ")}.`,
installedPackages,
);
}
if (error instanceof ClawPackageInstallError) {
throw new ClawPackageInstallError(error.code, error.message, installedPackages);
}
throw new ClawPackageInstallError("package_install_failed", message, installedPackages);
} finally {
try {
packageLease?.release();
} catch {
// Lease expiry recovers cleanup when the shared state database is unavailable.
}
}
}
return installedPackages;
}