fix(plugins): persist clawhub artifact metadata

This commit is contained in:
Vincent Koc
2026-05-02 10:08:05 -07:00
parent aafdc5945a
commit 7fae11b3b1
25 changed files with 268 additions and 93 deletions

View File

@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
- Codex/app-server: resolve managed binaries from bundled `dist` chunks and from the `@openai/codex` package bin when installs do not provide a nearby `.bin/codex` shim, avoiding false missing-binary startup failures.
- Plugins/source checkout: discover source-only plugins such as Codex from the `extensions/*` workspace while using npm package excludes as the packaged-core boundary, removing the stale core-bundle metadata path.
- Plugins/ClawHub: install ClawPack artifacts from the explicit npm-pack `.tgz` resolver path instead of the legacy ZIP-shaped placeholder route. Thanks @vincentkoc.
- Plugins/ClawHub: persist ClawHub artifact kind plus npm integrity, shasum, and tarball metadata on ClawPack install records for update and diagnostics flows. Thanks @vincentkoc.
- Control UI: allow deployments to configure grouped chat message max-width with a validated `gateway.controlUi.chatMessageMaxWidth` setting instead of patching bundled CSS after upgrades. Fixes #67935. Thanks @xiew4589-lang.
- Control UI/Cron: ignore malformed persisted cron rows without valid payloads before they enter UI state and guard stale cron render paths, preventing blank Control UI sections after a bad cron snapshot. Fixes #55047 and #54439; supersedes #54550 and #54552.
- Control UI/sessions: bound the default Sessions tab query to recent activity and fewer rows, avoiding expensive full-history loads while keeping filters editable. Fixes #76050. (#76051) Thanks @Neomail2.

View File

@@ -172,7 +172,7 @@ openclaw plugins install npm:openclaw-codex-app-server
openclaw plugins install npm:@scope/plugin-name@1.0.1
```
OpenClaw checks the advertised plugin API / minimum gateway compatibility before install. When the selected ClawHub version publishes a ClawPack artifact, OpenClaw downloads the versioned npm-pack `.tgz`, verifies the ClawHub digest header and the artifact digest, then installs it through the normal archive path. Older ClawHub versions without ClawPack metadata still install through the legacy package archive verification path. Recorded installs keep their ClawHub source metadata and ClawPack digest facts for later updates.
OpenClaw checks the advertised plugin API / minimum gateway compatibility before install. When the selected ClawHub version publishes a ClawPack artifact, OpenClaw downloads the versioned npm-pack `.tgz`, verifies the ClawHub digest header and the artifact digest, then installs it through the normal archive path. Older ClawHub versions without ClawPack metadata still install through the legacy package archive verification path. Recorded installs keep their ClawHub source metadata, artifact kind, npm integrity, npm shasum, tarball name, and ClawPack digest facts for later updates.
Unversioned ClawHub installs keep an unversioned recorded spec so `openclaw plugins update` can follow newer ClawHub releases; explicit version or tag selectors such as `clawhub:pkg@1.2.3` and `clawhub:pkg@beta` remain pinned to that selector.
#### Marketplace shorthand

View File

@@ -84,8 +84,9 @@ Site: [clawhub.ai](https://clawhub.ai)
`minGatewayVersion` compatibility before archive install runs, so
incompatible hosts fail closed early instead of partially installing
the package. When a package version publishes a ClawPack artifact,
OpenClaw prefers the exact uploaded npm-pack `.tgz`, verifies the ClawHub digest header and
downloaded bytes, and records the ClawPack digest metadata for later
OpenClaw prefers the exact uploaded npm-pack `.tgz`, verifies the ClawHub
digest header and downloaded bytes, and records the artifact kind, npm
integrity, npm shasum, tarball name, and ClawPack digest metadata for later
updates. Older package versions without ClawPack metadata still use the
legacy package archive verification path.

View File

@@ -392,6 +392,12 @@ function assertInstalled() {
if (!record.clawpackSha256 || typeof record.clawpackSize !== "number") {
throw new Error(`missing kitchen-sink ClawPack metadata: ${JSON.stringify(record)}`);
}
if (record.artifactKind !== "npm-pack" || record.artifactFormat !== "tgz") {
throw new Error(`missing kitchen-sink ClawHub artifact metadata: ${JSON.stringify(record)}`);
}
if (!record.npmIntegrity || !record.npmShasum || !record.npmTarballName) {
throw new Error(`missing kitchen-sink npm artifact metadata: ${JSON.stringify(record)}`);
}
}
if (typeof record.installPath !== "string" || record.installPath.length === 0) {
throw new Error("missing kitchen-sink install path");

View File

@@ -537,6 +537,14 @@ function assertClawHubInstalled() {
if (!record.clawpackSha256 || typeof record.clawpackSize !== "number") {
throw new Error(`missing ClawHub ClawPack metadata for ${pluginId}: ${JSON.stringify(record)}`);
}
if (record.artifactKind !== "npm-pack" || record.artifactFormat !== "tgz") {
throw new Error(`missing ClawHub artifact metadata for ${pluginId}: ${JSON.stringify(record)}`);
}
if (!record.npmIntegrity || !record.npmShasum || !record.npmTarballName) {
throw new Error(
`missing ClawHub npm artifact metadata for ${pluginId}: ${JSON.stringify(record)}`,
);
}
const installPath = record.installPath.replace(/^~(?=$|\/)/u, process.env.HOME);
const extensionsRoot = path.join(process.env.HOME, ".openclaw", "extensions");

View File

@@ -130,6 +130,11 @@ describe("plugins cli list", () => {
version: "2026.5.1",
clawhubPackage: "openclaw-mem0",
clawhubChannel: "official",
artifactKind: "npm-pack",
artifactFormat: "tgz",
npmIntegrity: "sha512-clawpack",
npmShasum: "1".repeat(40),
npmTarballName: "openclaw-mem0-2026.5.1.tgz",
clawpackSha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
clawpackSpecVersion: 1,
clawpackManifestSha256: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
@@ -175,6 +180,8 @@ describe("plugins cli list", () => {
expect(runtimeLogs.join("\n")).toContain("Policy");
expect(runtimeLogs.join("\n")).toContain("allowConversationAccess: true");
expect(runtimeLogs.join("\n")).toContain("ClawHub package: openclaw-mem0");
expect(runtimeLogs.join("\n")).toContain("Artifact kind: npm-pack");
expect(runtimeLogs.join("\n")).toContain("Npm integrity: sha512-clawpack");
expect(runtimeLogs.join("\n")).toContain(
"ClawPack sha256: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
);

View File

@@ -77,6 +77,21 @@ function formatInstallLines(install: PluginInstallRecord | undefined): string[]
if (install.clawhubChannel) {
lines.push(`ClawHub channel: ${install.clawhubChannel}`);
}
if (install.artifactKind) {
lines.push(`Artifact kind: ${install.artifactKind}`);
}
if (install.artifactFormat) {
lines.push(`Artifact format: ${install.artifactFormat}`);
}
if (install.npmIntegrity) {
lines.push(`Npm integrity: ${install.npmIntegrity}`);
}
if (install.npmShasum) {
lines.push(`Npm shasum: ${install.npmShasum}`);
}
if (install.npmTarballName) {
lines.push(`Npm tarball: ${install.npmTarballName}`);
}
if (install.clawpackSha256) {
lines.push(`ClawPack sha256: ${install.clawpackSha256}`);
}

View File

@@ -7,6 +7,7 @@ import { resolveArchiveKind } from "../infra/archive.js";
import { parseClawHubPluginSpec } from "../infra/clawhub.js";
import { formatErrorMessage } from "../infra/errors.js";
import { type BundledPluginSource, findBundledPluginSource } from "../plugins/bundled-sources.js";
import { buildClawHubPluginInstallRecordFields } from "../plugins/clawhub-install-records.js";
import { installPluginFromClawHub } from "../plugins/clawhub.js";
import { installPluginFromGitSpec, parseGitPluginSpec } from "../plugins/git-install.js";
import { resolveDefaultPluginExtensionsDir } from "../plugins/install-paths.js";
@@ -766,20 +767,9 @@ export async function runPluginInstallCommand(params: {
snapshot,
pluginId: result.pluginId,
install: {
source: "clawhub",
...buildClawHubPluginInstallRecordFields(result.clawhub),
spec: raw,
installPath: result.targetDir,
version: result.version,
integrity: result.clawhub.integrity,
resolvedAt: result.clawhub.resolvedAt,
clawhubUrl: result.clawhub.clawhubUrl,
clawhubPackage: result.clawhub.clawhubPackage,
clawhubFamily: result.clawhub.clawhubFamily,
clawhubChannel: result.clawhub.clawhubChannel,
clawpackSha256: result.clawhub.clawpackSha256,
clawpackSpecVersion: result.clawhub.clawpackSpecVersion,
clawpackManifestSha256: result.clawhub.clawpackManifestSha256,
clawpackSize: result.clawhub.clawpackSize,
},
runtime,
});
@@ -800,20 +790,9 @@ export async function runPluginInstallCommand(params: {
snapshot,
pluginId: clawhubResult.pluginId,
install: {
source: "clawhub",
...buildClawHubPluginInstallRecordFields(clawhubResult.clawhub),
spec: preferredClawHubSpec,
installPath: clawhubResult.targetDir,
version: clawhubResult.version,
integrity: clawhubResult.clawhub.integrity,
resolvedAt: clawhubResult.clawhub.resolvedAt,
clawhubUrl: clawhubResult.clawhub.clawhubUrl,
clawhubPackage: clawhubResult.clawhub.clawhubPackage,
clawhubFamily: clawhubResult.clawhub.clawhubFamily,
clawhubChannel: clawhubResult.clawhub.clawhubChannel,
clawpackSha256: clawhubResult.clawhub.clawpackSha256,
clawpackSpecVersion: clawhubResult.clawhub.clawpackSpecVersion,
clawpackManifestSha256: clawhubResult.clawhub.clawpackManifestSha256,
clawpackSize: clawhubResult.clawhub.clawpackSize,
},
runtime,
});

View File

@@ -2,6 +2,7 @@ import { listChannelPluginCatalogEntries } from "../../../channels/plugins/catal
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
import type { PluginInstallRecord } from "../../../config/types.plugins.js";
import { parseRegistryNpmSpec } from "../../../infra/npm-registry-spec.js";
import { buildClawHubPluginInstallRecordFields } from "../../../plugins/clawhub-install-records.js";
import { CLAWHUB_INSTALL_ERROR_CODE, installPluginFromClawHub } from "../../../plugins/clawhub.js";
import { resolveDefaultPluginExtensionsDir } from "../../../plugins/install-paths.js";
import { installPluginFromNpmSpec } from "../../../plugins/install.js";
@@ -169,21 +170,10 @@ async function installCandidate(params: {
records: {
...params.records,
[pluginId]: {
source: "clawhub",
...buildClawHubPluginInstallRecordFields(clawhubResult.clawhub),
spec: candidate.clawhubSpec,
installPath: clawhubResult.targetDir,
version: clawhubResult.version,
installedAt: new Date().toISOString(),
integrity: clawhubResult.clawhub.integrity,
resolvedAt: clawhubResult.clawhub.resolvedAt,
clawhubUrl: clawhubResult.clawhub.clawhubUrl,
clawhubPackage: clawhubResult.clawhub.clawhubPackage,
clawhubFamily: clawhubResult.clawhub.clawhubFamily,
clawhubChannel: clawhubResult.clawhub.clawhubChannel,
clawpackSha256: clawhubResult.clawhub.clawpackSha256,
clawpackSpecVersion: clawhubResult.clawhub.clawpackSpecVersion,
clawpackManifestSha256: clawhubResult.clawhub.clawpackManifestSha256,
clawpackSize: clawhubResult.clawhub.clawpackSize,
},
},
changes: [

View File

@@ -8,6 +8,7 @@ import {
findBundledPluginSourceInMap,
resolveBundledPluginSources,
} from "../plugins/bundled-sources.js";
import { buildClawHubPluginInstallRecordFields } from "../plugins/clawhub-install-records.js";
import { enablePluginInConfig, type PluginEnableResult } from "../plugins/enable.js";
import { resolveDefaultPluginExtensionsDir } from "../plugins/install-paths.js";
import { installPluginFromNpmSpec } from "../plugins/install.js";
@@ -823,20 +824,9 @@ export async function ensureOnboardingPluginInstalled(params: {
next = enableResult.config;
next = recordPluginInstall(next, {
pluginId: result.pluginId,
source: "clawhub",
...buildClawHubPluginInstallRecordFields(result.clawhub),
spec: clawhubSpec,
installPath: result.targetDir,
version: result.version,
integrity: result.clawhub.integrity,
resolvedAt: result.clawhub.resolvedAt,
clawhubUrl: result.clawhub.clawhubUrl,
clawhubPackage: result.clawhub.clawhubPackage,
clawhubFamily: result.clawhub.clawhubFamily,
clawhubChannel: result.clawhub.clawhubChannel,
clawpackSha256: result.clawhub.clawpackSha256,
clawpackSpecVersion: result.clawhub.clawpackSpecVersion,
clawpackManifestSha256: result.clawhub.clawpackManifestSha256,
clawpackSize: result.clawhub.clawpackSize,
});
return {
cfg: next,

View File

@@ -15,6 +15,11 @@ export type InstallRecordBase = {
clawhubPackage?: string;
clawhubFamily?: "code-plugin" | "bundle-plugin";
clawhubChannel?: "official" | "community" | "private";
artifactKind?: "legacy-zip" | "npm-pack";
artifactFormat?: "zip" | "tgz";
npmIntegrity?: string;
npmShasum?: string;
npmTarballName?: string;
clawpackSha256?: string;
clawpackSpecVersion?: number;
clawpackManifestSha256?: string;

View File

@@ -247,6 +247,7 @@ describe("clawhub helpers", () => {
it("downloads ClawPack package artifacts from the version route and verifies response headers", async () => {
const bytes = new Uint8Array([7, 8, 9]);
const sha256Hex = createHash("sha256").update(bytes).digest("hex");
const sha1Hex = createHash("sha1").update(bytes).digest("hex");
let requestedUrl = "";
const archive = await downloadClawHubPackageArchive({
name: "demo",
@@ -273,6 +274,7 @@ describe("clawhub helpers", () => {
expect(archive.sha256Hex).toBe(sha256Hex);
expect(archive.clawpackHeaderSha256).toBe(sha256Hex);
expect(archive.npmIntegrity).toMatch(/^sha512-/);
expect(archive.npmShasum).toBe(sha1Hex);
await expect(fs.readFile(archive.archivePath)).resolves.toEqual(Buffer.from(bytes));
} finally {
const archiveDir = path.dirname(archive.archivePath);

View File

@@ -504,6 +504,10 @@ function formatSha512Integrity(bytes: Uint8Array): string {
return `sha512-${digest}`;
}
function formatSha1Hex(bytes: Uint8Array): string {
return createHash("sha1").update(bytes).digest("hex");
}
function normalizeHeaderValue(value: string | null): string | undefined {
const normalized = normalizeOptionalString(value);
return normalized && normalized.length > 0 ? normalized : undefined;
@@ -701,6 +705,7 @@ export async function downloadClawHubPackageArchive(params: {
const bytes = new Uint8Array(await response.arrayBuffer());
const sha256Hex = formatSha256Hex(bytes);
const npmIntegrity = formatSha512Integrity(bytes);
const npmShasum = formatSha1Hex(bytes);
const headerSha256 = normalizeClawHubSha256Hex(
response.headers.get("X-ClawHub-Artifact-Sha256") ??
response.headers.get("X-ClawHub-ClawPack-Sha256") ??
@@ -724,6 +729,12 @@ export async function downloadClawHubPackageArchive(params: {
`ClawHub ClawPack download for "${params.name}@${params.version}" declared npm integrity ${headerNpmIntegrity}, got ${npmIntegrity}.`,
);
}
const headerNpmShasum = normalizeHeaderValue(response.headers.get("X-ClawHub-Npm-Shasum"));
if (headerNpmShasum && headerNpmShasum !== npmShasum) {
throw new Error(
`ClawHub ClawPack download for "${params.name}@${params.version}" declared npm shasum ${headerNpmShasum}, got ${npmShasum}.`,
);
}
const npmTarballName =
normalizeHeaderValue(response.headers.get("X-ClawHub-Npm-Tarball-Name")) ??
safePackageTarballName(params.name, params.version);
@@ -745,9 +756,7 @@ export async function downloadClawHubPackageArchive(params: {
? { clawpackHeaderSpecVersion: specVersion }
: {}),
npmIntegrity,
...(normalizeHeaderValue(response.headers.get("X-ClawHub-Npm-Shasum"))
? { npmShasum: normalizeHeaderValue(response.headers.get("X-ClawHub-Npm-Shasum")) }
: {}),
npmShasum,
npmTarballName,
cleanup: target.cleanup,
};

View File

@@ -0,0 +1,72 @@
import type { PluginInstallRecord } from "../config/types.plugins.js";
import type { ClawHubPackageChannel, ClawHubPackageFamily } from "../infra/clawhub.js";
export type ClawHubPluginInstallRecordFields = {
source: "clawhub";
clawhubUrl: string;
clawhubPackage: string;
clawhubFamily: Exclude<ClawHubPackageFamily, "skill">;
clawhubChannel?: ClawHubPackageChannel;
version?: string;
integrity?: string;
resolvedAt?: string;
installedAt?: string;
artifactKind?: "legacy-zip" | "npm-pack";
artifactFormat?: "zip" | "tgz";
npmIntegrity?: string;
npmShasum?: string;
npmTarballName?: string;
clawpackSha256?: string;
clawpackSpecVersion?: number;
clawpackManifestSha256?: string;
clawpackSize?: number;
};
export function buildClawHubPluginInstallRecordFields(
fields: ClawHubPluginInstallRecordFields,
): Pick<
PluginInstallRecord,
| "source"
| "clawhubUrl"
| "clawhubPackage"
| "clawhubFamily"
| "clawhubChannel"
| "version"
| "integrity"
| "resolvedAt"
| "installedAt"
| "artifactKind"
| "artifactFormat"
| "npmIntegrity"
| "npmShasum"
| "npmTarballName"
| "clawpackSha256"
| "clawpackSpecVersion"
| "clawpackManifestSha256"
| "clawpackSize"
> {
return {
source: "clawhub",
clawhubUrl: fields.clawhubUrl,
clawhubPackage: fields.clawhubPackage,
clawhubFamily: fields.clawhubFamily,
...(fields.clawhubChannel ? { clawhubChannel: fields.clawhubChannel } : {}),
...(fields.version ? { version: fields.version } : {}),
...(fields.integrity ? { integrity: fields.integrity } : {}),
...(fields.resolvedAt ? { resolvedAt: fields.resolvedAt } : {}),
...(fields.installedAt ? { installedAt: fields.installedAt } : {}),
...(fields.artifactKind ? { artifactKind: fields.artifactKind } : {}),
...(fields.artifactFormat ? { artifactFormat: fields.artifactFormat } : {}),
...(fields.npmIntegrity ? { npmIntegrity: fields.npmIntegrity } : {}),
...(fields.npmShasum ? { npmShasum: fields.npmShasum } : {}),
...(fields.npmTarballName ? { npmTarballName: fields.npmTarballName } : {}),
...(fields.clawpackSha256 ? { clawpackSha256: fields.clawpackSha256 } : {}),
...(fields.clawpackSpecVersion !== undefined
? { clawpackSpecVersion: fields.clawpackSpecVersion }
: {}),
...(fields.clawpackManifestSha256
? { clawpackManifestSha256: fields.clawpackManifestSha256 }
: {}),
...(fields.clawpackSize !== undefined ? { clawpackSize: fields.clawpackSize } : {}),
};
}

View File

@@ -352,6 +352,8 @@ describe("installPluginFromClawHub", () => {
artifact: "clawpack",
clawpackHeaderSha256: DEMO_CLAWPACK_SHA256,
npmIntegrity: "sha512-clawpack",
npmShasum: "1".repeat(40),
npmTarballName: "demo-2026.3.22.tgz",
cleanup: archiveCleanupMock,
});
@@ -364,6 +366,11 @@ describe("installPluginFromClawHub", () => {
ok: true,
clawhub: {
integrity: DEMO_CLAWPACK_INTEGRITY,
artifactKind: "npm-pack",
artifactFormat: "tgz",
npmIntegrity: "sha512-clawpack",
npmShasum: "1".repeat(40),
npmTarballName: "demo-2026.3.22.tgz",
clawpackSha256: DEMO_CLAWPACK_SHA256,
clawpackSize: 4096,
},

View File

@@ -32,6 +32,7 @@ import {
import { formatErrorMessage } from "../infra/errors.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { resolveCompatibilityHostVersion } from "../version.js";
import type { ClawHubPluginInstallRecordFields } from "./clawhub-install-records.js";
import type { InstallSafetyOverrides } from "./install-security-scan.js";
import { installPluginFromArchive, type InstallPluginResult } from "./install.js";
@@ -57,22 +58,6 @@ type PluginInstallLogger = {
warn?: (message: string) => void;
};
export type ClawHubPluginInstallRecordFields = {
source: "clawhub";
clawhubUrl: string;
clawhubPackage: string;
clawhubFamily: Exclude<ClawHubPackageFamily, "skill">;
clawhubChannel?: ClawHubPackageChannel;
version?: string;
integrity?: string;
resolvedAt?: string;
installedAt?: string;
clawpackSha256?: string;
clawpackSpecVersion?: number;
clawpackManifestSha256?: string;
clawpackSize?: number;
};
type ClawHubInstallFailure = {
ok: false;
error: string;
@@ -132,7 +117,15 @@ function normalizeClawHubClawPackInstallFields(
clawpack: ClawHubPackageArtifactSummary | ClawHubPackageClawPackSummary | null | undefined,
): Pick<
ClawHubPluginInstallRecordFields,
"clawpackSha256" | "clawpackSpecVersion" | "clawpackManifestSha256" | "clawpackSize"
| "artifactKind"
| "artifactFormat"
| "npmIntegrity"
| "npmShasum"
| "npmTarballName"
| "clawpackSha256"
| "clawpackSpecVersion"
| "clawpackManifestSha256"
| "clawpackSize"
> {
const isNpmPackArtifact =
clawpack && "kind" in clawpack && normalizeOptionalString(clawpack.kind) === "npm-pack";
@@ -158,7 +151,15 @@ function normalizeClawHubClawPackInstallFields(
typeof clawpack.size === "number" && Number.isSafeInteger(clawpack.size) && clawpack.size >= 0
? clawpack.size
: undefined;
const npmIntegrity = normalizeOptionalString(clawpack.npmIntegrity);
const npmShasum = normalizeOptionalString(clawpack.npmShasum);
const npmTarballName = normalizeOptionalString(clawpack.npmTarballName);
return {
artifactKind: "npm-pack",
artifactFormat: "tgz",
...(npmIntegrity ? { npmIntegrity } : {}),
...(npmShasum ? { npmShasum } : {}),
...(npmTarballName ? { npmTarballName } : {}),
...(clawpackSha256 ? { clawpackSha256 } : {}),
...(clawpackSpecVersion !== undefined ? { clawpackSpecVersion } : {}),
...(clawpackManifestSha256 ? { clawpackManifestSha256 } : {}),
@@ -196,6 +197,18 @@ function resolveClawHubNpmIntegrity(
return normalizeOptionalString(clawpack?.npmIntegrity) ?? null;
}
function resolveClawHubNpmShasum(
clawpack: ClawHubPackageArtifactSummary | ClawHubPackageClawPackSummary | null | undefined,
): string | null {
return normalizeOptionalString(clawpack?.npmShasum) ?? null;
}
function resolveClawHubNpmTarballName(
clawpack: ClawHubPackageArtifactSummary | ClawHubPackageClawPackSummary | null | undefined,
): string | null {
return normalizeOptionalString(clawpack?.npmTarballName) ?? null;
}
function resolveClawHubNpmPackArtifact(
version: NonNullable<ClawHubPackageVersion["version"]>,
): ClawHubPackageArtifactSummary | ClawHubPackageClawPackSummary | null {
@@ -956,6 +969,13 @@ export async function installPluginFromClawHub(
CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH,
);
}
const expectedNpmShasum = resolveClawHubNpmShasum(versionState.clawpack);
if (expectedNpmShasum && archive.npmShasum !== expectedNpmShasum) {
return buildClawHubInstallFailure(
`ClawHub ClawPack npm shasum mismatch for "${parsed.name}@${versionState.version}": expected ${expectedNpmShasum}, got ${archive.npmShasum ?? "unknown"}.`,
CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH,
);
}
} else if (versionState.verification?.kind === "archive-integrity") {
if (archive.integrity !== versionState.verification.integrity) {
return buildClawHubInstallFailure(
@@ -1005,6 +1025,20 @@ export async function installPluginFromClawHub(
const pkg = detail.package!;
const clawpackFields = normalizeClawHubClawPackInstallFields(versionState.clawpack);
const observedClawPackArtifactFields =
archive.artifact === "clawpack"
? ({
artifactKind: "npm-pack",
artifactFormat: "tgz",
...(archive.npmIntegrity ? { npmIntegrity: archive.npmIntegrity } : {}),
...(archive.npmShasum ? { npmShasum: archive.npmShasum } : {}),
...(archive.npmTarballName ? { npmTarballName: archive.npmTarballName } : {}),
} satisfies Partial<ClawHubPluginInstallRecordFields>)
: ({
artifactKind: "legacy-zip",
artifactFormat: "zip",
} satisfies Partial<ClawHubPluginInstallRecordFields>);
const expectedTarballName = resolveClawHubNpmTarballName(versionState.clawpack);
const clawhubFamily =
pkg.family === "code-plugin" || pkg.family === "bundle-plugin" ? pkg.family : null;
if (!clawhubFamily) {
@@ -1031,6 +1065,10 @@ export async function installPluginFromClawHub(
integrity: archive.integrity,
resolvedAt: new Date().toISOString(),
...clawpackFields,
...observedClawPackArtifactFields,
...(expectedTarballName && !archive.npmTarballName
? { npmTarballName: expectedTarballName }
: {}),
},
};
} finally {

View File

@@ -52,6 +52,11 @@ function normalizeInstallRecord(
setInstallStringField(normalized, "clawhubPackage", record.clawhubPackage);
setInstallStringField(normalized, "clawhubFamily", record.clawhubFamily);
setInstallStringField(normalized, "clawhubChannel", record.clawhubChannel);
setInstallStringField(normalized, "artifactKind", record.artifactKind);
setInstallStringField(normalized, "artifactFormat", record.artifactFormat);
setInstallStringField(normalized, "npmIntegrity", record.npmIntegrity);
setInstallStringField(normalized, "npmShasum", record.npmShasum);
setInstallStringField(normalized, "npmTarballName", record.npmTarballName);
setInstallStringField(normalized, "clawpackSha256", record.clawpackSha256);
setInstallNumberField(normalized, "clawpackSpecVersion", record.clawpackSpecVersion);
setInstallStringField(normalized, "clawpackManifestSha256", record.clawpackManifestSha256);

View File

@@ -205,6 +205,11 @@ describe("plugin index install records store", () => {
clawhubPackage: "clawpack-demo",
clawhubFamily: "code-plugin",
clawhubChannel: "official",
artifactKind: "npm-pack",
artifactFormat: "tgz",
npmIntegrity: "sha512-clawpack",
npmShasum: "1".repeat(40),
npmTarballName: "clawpack-demo-2026.5.1-beta.2.tgz",
clawpackSha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
clawpackSpecVersion: 1,
clawpackManifestSha256:
@@ -219,6 +224,11 @@ describe("plugin index install records store", () => {
"clawpack-demo": {
source: "clawhub",
spec: "clawhub:clawpack-demo",
artifactKind: "npm-pack",
artifactFormat: "tgz",
npmIntegrity: "sha512-clawpack",
npmShasum: "1".repeat(40),
npmTarballName: "clawpack-demo-2026.5.1-beta.2.tgz",
clawpackSha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
clawpackSpecVersion: 1,
clawpackManifestSha256: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",

View File

@@ -438,6 +438,11 @@ describe("installed plugin index persistence", () => {
clawhubPackage: "clawpack-demo",
clawhubFamily: "code-plugin",
clawhubChannel: "official",
artifactKind: "npm-pack",
artifactFormat: "tgz",
npmIntegrity: "sha512-clawpack",
npmShasum: "1".repeat(40),
npmTarballName: "clawpack-demo-2026.5.1-beta.2.tgz",
clawpackSha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
clawpackSpecVersion: 1,
clawpackManifestSha256:
@@ -474,6 +479,11 @@ describe("installed plugin index persistence", () => {
clawhubPackage: "clawpack-demo",
clawhubFamily: "code-plugin",
clawhubChannel: "official",
artifactKind: "npm-pack",
artifactFormat: "tgz",
npmIntegrity: "sha512-clawpack",
npmShasum: "1".repeat(40),
npmTarballName: "clawpack-demo-2026.5.1-beta.2.tgz",
clawpackSha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
clawpackSpecVersion: 1,
clawpackManifestSha256:

View File

@@ -49,6 +49,11 @@ export type InstalledPluginInstallRecordInfo = Pick<
| "clawhubPackage"
| "clawhubFamily"
| "clawhubChannel"
| "artifactKind"
| "artifactFormat"
| "npmIntegrity"
| "npmShasum"
| "npmTarballName"
| "clawpackSha256"
| "clawpackSpecVersion"
| "clawpackManifestSha256"

View File

@@ -1056,6 +1056,11 @@ describe("uninstallPlugin", () => {
clawhubPackage: "clawpack-demo",
clawhubFamily: "code-plugin",
clawhubChannel: "official",
artifactKind: "npm-pack",
artifactFormat: "tgz",
npmIntegrity: "sha512-clawpack",
npmShasum: "1".repeat(40),
npmTarballName: "clawpack-demo-2026.5.1-beta.2.tgz",
clawpackSha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
clawpackSpecVersion: 1,
clawpackManifestSha256:
@@ -1250,6 +1255,11 @@ describe("resolveUninstallDirectoryTarget", () => {
clawhubPackage: "clawpack-demo",
clawhubFamily: "code-plugin",
clawhubChannel: "official",
artifactKind: "npm-pack",
artifactFormat: "tgz",
npmIntegrity: "sha512-clawpack",
npmShasum: "1".repeat(40),
npmTarballName: "clawpack-demo-2026.5.1-beta.2.tgz",
clawpackSha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
clawpackSpecVersion: 1,
clawpackManifestSha256:

View File

@@ -100,6 +100,11 @@ function createSuccessfulClawHubUpdateResult(params?: {
version: params?.version ?? "2026.5.1-beta.2",
integrity: "sha256-clawpack",
resolvedAt: "2026-05-01T00:00:00.000Z",
artifactKind: "npm-pack" as const,
artifactFormat: "tgz" as const,
npmIntegrity: "sha512-clawpack",
npmShasum: "2".repeat(40),
npmTarballName: `${params?.clawhubPackage ?? "legacy-chat"}-${params?.version ?? "2026.5.1-beta.2"}.tgz`,
clawpackSha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
clawpackSpecVersion: 1,
clawpackManifestSha256: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
@@ -1036,6 +1041,11 @@ describe("updateNpmInstalledPlugins", () => {
clawhubPackage: "demo",
clawhubFamily: "code-plugin",
clawhubChannel: "official",
artifactKind: "npm-pack",
artifactFormat: "tgz",
npmIntegrity: "sha512-next",
npmShasum: "1".repeat(40),
npmTarballName: "demo-1.2.4.tgz",
integrity: "sha256-next",
resolvedAt: "2026-03-22T00:00:00.000Z",
clawpackSha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
@@ -1075,6 +1085,11 @@ describe("updateNpmInstalledPlugins", () => {
clawhubPackage: "demo",
clawhubFamily: "code-plugin",
clawhubChannel: "official",
artifactKind: "npm-pack",
artifactFormat: "tgz",
npmIntegrity: "sha512-next",
npmShasum: "1".repeat(40),
npmTarballName: "demo-1.2.4.tgz",
integrity: "sha256-next",
clawpackSha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
clawpackSpecVersion: 1,
@@ -1764,6 +1779,11 @@ describe("syncPluginsForUpdateChannel", () => {
clawhubPackage: "legacy-chat",
clawhubFamily: "code-plugin",
clawhubChannel: "official",
artifactKind: "npm-pack",
artifactFormat: "tgz",
npmIntegrity: "sha512-clawpack",
npmShasum: "2".repeat(40),
npmTarballName: "legacy-chat-2026.5.1-beta.2.tgz",
clawpackSha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
clawpackSpecVersion: 1,
clawpackManifestSha256: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",

View File

@@ -11,6 +11,7 @@ import { compareComparableSemver, parseComparableSemver } from "../infra/semver-
import type { UpdateChannel } from "../infra/update-channels.js";
import { resolveUserPath } from "../utils.js";
import { resolveBundledPluginSources } from "./bundled-sources.js";
import { buildClawHubPluginInstallRecordFields } from "./clawhub-install-records.js";
import { CLAWHUB_INSTALL_ERROR_CODE, installPluginFromClawHub } from "./clawhub.js";
import { normalizePluginsConfig, resolveEffectiveEnableState } from "./config-state.js";
import {
@@ -953,20 +954,10 @@ export async function updateNpmInstalledPlugins(params: {
>;
next = recordPluginInstall(next, {
pluginId: resolvedPluginId,
source: "clawhub",
...buildClawHubPluginInstallRecordFields(clawhubResult.clawhub),
spec: effectiveSpec ?? record.spec ?? `clawhub:${record.clawhubPackage!}`,
installPath: result.targetDir,
version: nextVersion,
integrity: clawhubResult.clawhub.integrity,
resolvedAt: clawhubResult.clawhub.resolvedAt,
clawhubUrl: clawhubResult.clawhub.clawhubUrl,
clawhubPackage: clawhubResult.clawhub.clawhubPackage,
clawhubFamily: clawhubResult.clawhub.clawhubFamily,
clawhubChannel: clawhubResult.clawhub.clawhubChannel,
clawpackSha256: clawhubResult.clawhub.clawpackSha256,
clawpackSpecVersion: clawhubResult.clawhub.clawpackSpecVersion,
clawpackManifestSha256: clawhubResult.clawhub.clawpackManifestSha256,
clawpackSize: clawhubResult.clawhub.clawpackSize,
});
} else if (record.source === "git") {
const gitResult = result as Extract<
@@ -1211,20 +1202,10 @@ export async function syncPluginsForUpdateChannel(params: {
>;
next = recordPluginInstall(next, {
pluginId: resolvedPluginId,
source: "clawhub",
...buildClawHubPluginInstallRecordFields(clawhubResult.clawhub),
spec: installSpec,
installPath: result.targetDir,
version: nextVersion,
integrity: clawhubResult.clawhub.integrity,
resolvedAt: clawhubResult.clawhub.resolvedAt,
clawhubUrl: clawhubResult.clawhub.clawhubUrl,
clawhubPackage: clawhubResult.clawhub.clawhubPackage,
clawhubFamily: clawhubResult.clawhub.clawhubFamily,
clawhubChannel: clawhubResult.clawhub.clawhubChannel,
clawpackSha256: clawhubResult.clawhub.clawpackSha256,
clawpackSpecVersion: clawhubResult.clawhub.clawpackSpecVersion,
clawpackManifestSha256: clawhubResult.clawhub.clawpackManifestSha256,
clawpackSize: clawhubResult.clawhub.clawpackSize,
});
} else {
const npmResult = result as Extract<

View File

@@ -308,5 +308,7 @@ describe("docker build helper", () => {
expect(clawhub).toContain("clawhub:@openclaw/kitchen-sink");
expect(assertions).toContain("clawhub-updated");
expect(assertions).toContain("record.clawpackSha256");
expect(assertions).toContain("record.artifactKind");
expect(assertions).toContain("record.npmIntegrity");
});
});

View File

@@ -132,6 +132,8 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => {
expect(assertionsScript).toContain("record.source !== source");
expect(assertionsScript).toContain("record.clawhubPackage !== packageName");
expect(assertionsScript).toContain("record.clawpackSha256");
expect(assertionsScript).toContain("record.artifactKind");
expect(assertionsScript).toContain("record.npmIntegrity");
expect(assertionsScript).toContain("assertClawHubExternalInstallContract");
expect(assertionsScript).toContain("expectedErrorMessages");
expect(assertionsScript).toContain(