From 7fae11b3b1e4f8831eedfbc965c3f5e554c240c4 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 2 May 2026 10:08:05 -0700 Subject: [PATCH] fix(plugins): persist clawhub artifact metadata --- CHANGELOG.md | 1 + docs/cli/plugins.md | 2 +- docs/tools/clawhub.md | 5 +- .../lib/kitchen-sink-plugin/assertions.mjs | 6 ++ scripts/e2e/lib/plugins/assertions.mjs | 8 +++ src/cli/plugins-cli.list.test.ts | 7 ++ src/cli/plugins-inspect-command.ts | 15 ++++ src/cli/plugins-install-command.ts | 27 +------ .../missing-configured-plugin-install.ts | 14 +--- src/commands/onboarding-plugin-install.ts | 14 +--- src/config/types.installs.ts | 5 ++ src/infra/clawhub.test.ts | 2 + src/infra/clawhub.ts | 15 +++- src/plugins/clawhub-install-records.ts | 72 +++++++++++++++++++ src/plugins/clawhub.test.ts | 7 ++ src/plugins/clawhub.ts | 72 ++++++++++++++----- .../installed-plugin-index-install-records.ts | 5 ++ .../installed-plugin-index-records.test.ts | 10 +++ .../installed-plugin-index-store.test.ts | 10 +++ src/plugins/installed-plugin-index-types.ts | 5 ++ src/plugins/uninstall.test.ts | 10 +++ src/plugins/update.test.ts | 20 ++++++ src/plugins/update.ts | 25 +------ test/scripts/docker-build-helper.test.ts | 2 + .../plugin-prerelease-test-plan.test.ts | 2 + 25 files changed, 268 insertions(+), 93 deletions(-) create mode 100644 src/plugins/clawhub-install-records.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 71871bddd7c..c72111c235d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/docs/cli/plugins.md b/docs/cli/plugins.md index b8bfbacf7d8..814fffcd9d7 100644 --- a/docs/cli/plugins.md +++ b/docs/cli/plugins.md @@ -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 diff --git a/docs/tools/clawhub.md b/docs/tools/clawhub.md index cc025cc1deb..2b9fdbdebaa 100644 --- a/docs/tools/clawhub.md +++ b/docs/tools/clawhub.md @@ -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. diff --git a/scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs b/scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs index 6f1fcb6ed65..02f9a487852 100644 --- a/scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs +++ b/scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs @@ -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"); diff --git a/scripts/e2e/lib/plugins/assertions.mjs b/scripts/e2e/lib/plugins/assertions.mjs index 7774034bbb5..e8ec17f6ea9 100644 --- a/scripts/e2e/lib/plugins/assertions.mjs +++ b/scripts/e2e/lib/plugins/assertions.mjs @@ -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"); diff --git a/src/cli/plugins-cli.list.test.ts b/src/cli/plugins-cli.list.test.ts index a8ed65f4dd2..ed45cb96a08 100644 --- a/src/cli/plugins-cli.list.test.ts +++ b/src/cli/plugins-cli.list.test.ts @@ -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", ); diff --git a/src/cli/plugins-inspect-command.ts b/src/cli/plugins-inspect-command.ts index 60fead43655..51b49779f64 100644 --- a/src/cli/plugins-inspect-command.ts +++ b/src/cli/plugins-inspect-command.ts @@ -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}`); } diff --git a/src/cli/plugins-install-command.ts b/src/cli/plugins-install-command.ts index 52aef877a35..bd5877db45f 100644 --- a/src/cli/plugins-install-command.ts +++ b/src/cli/plugins-install-command.ts @@ -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, }); diff --git a/src/commands/doctor/shared/missing-configured-plugin-install.ts b/src/commands/doctor/shared/missing-configured-plugin-install.ts index a7c8aaa97b9..d6d17fc3aba 100644 --- a/src/commands/doctor/shared/missing-configured-plugin-install.ts +++ b/src/commands/doctor/shared/missing-configured-plugin-install.ts @@ -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: [ diff --git a/src/commands/onboarding-plugin-install.ts b/src/commands/onboarding-plugin-install.ts index b67bc9d5419..4b28b9ea7d0 100644 --- a/src/commands/onboarding-plugin-install.ts +++ b/src/commands/onboarding-plugin-install.ts @@ -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, diff --git a/src/config/types.installs.ts b/src/config/types.installs.ts index ccafbdfb505..adc90daa1cc 100644 --- a/src/config/types.installs.ts +++ b/src/config/types.installs.ts @@ -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; diff --git a/src/infra/clawhub.test.ts b/src/infra/clawhub.test.ts index 4e3d41c388b..0b2a02b5edf 100644 --- a/src/infra/clawhub.test.ts +++ b/src/infra/clawhub.test.ts @@ -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); diff --git a/src/infra/clawhub.ts b/src/infra/clawhub.ts index 54c7e98dfbb..3241d3e9066 100644 --- a/src/infra/clawhub.ts +++ b/src/infra/clawhub.ts @@ -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, }; diff --git a/src/plugins/clawhub-install-records.ts b/src/plugins/clawhub-install-records.ts new file mode 100644 index 00000000000..8eb7adcfc58 --- /dev/null +++ b/src/plugins/clawhub-install-records.ts @@ -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; + 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 } : {}), + }; +} diff --git a/src/plugins/clawhub.test.ts b/src/plugins/clawhub.test.ts index eeb40b42b1e..13eb4103d86 100644 --- a/src/plugins/clawhub.test.ts +++ b/src/plugins/clawhub.test.ts @@ -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, }, diff --git a/src/plugins/clawhub.ts b/src/plugins/clawhub.ts index 0d9dc4ce67e..637ebc93aa8 100644 --- a/src/plugins/clawhub.ts +++ b/src/plugins/clawhub.ts @@ -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; - 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, ): 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) + : ({ + artifactKind: "legacy-zip", + artifactFormat: "zip", + } satisfies Partial); + 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 { diff --git a/src/plugins/installed-plugin-index-install-records.ts b/src/plugins/installed-plugin-index-install-records.ts index 923b5c38e15..65b3e0a1f34 100644 --- a/src/plugins/installed-plugin-index-install-records.ts +++ b/src/plugins/installed-plugin-index-install-records.ts @@ -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); diff --git a/src/plugins/installed-plugin-index-records.test.ts b/src/plugins/installed-plugin-index-records.test.ts index 74636bc6fe3..44f87d9e06e 100644 --- a/src/plugins/installed-plugin-index-records.test.ts +++ b/src/plugins/installed-plugin-index-records.test.ts @@ -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", diff --git a/src/plugins/installed-plugin-index-store.test.ts b/src/plugins/installed-plugin-index-store.test.ts index 0f55b54e298..604ba5b71d2 100644 --- a/src/plugins/installed-plugin-index-store.test.ts +++ b/src/plugins/installed-plugin-index-store.test.ts @@ -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: diff --git a/src/plugins/installed-plugin-index-types.ts b/src/plugins/installed-plugin-index-types.ts index eee98adab7e..7fc86f2586c 100644 --- a/src/plugins/installed-plugin-index-types.ts +++ b/src/plugins/installed-plugin-index-types.ts @@ -49,6 +49,11 @@ export type InstalledPluginInstallRecordInfo = Pick< | "clawhubPackage" | "clawhubFamily" | "clawhubChannel" + | "artifactKind" + | "artifactFormat" + | "npmIntegrity" + | "npmShasum" + | "npmTarballName" | "clawpackSha256" | "clawpackSpecVersion" | "clawpackManifestSha256" diff --git a/src/plugins/uninstall.test.ts b/src/plugins/uninstall.test.ts index 9f36de1dfab..a5d408d93e1 100644 --- a/src/plugins/uninstall.test.ts +++ b/src/plugins/uninstall.test.ts @@ -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: diff --git a/src/plugins/update.test.ts b/src/plugins/update.test.ts index b4841713025..508940cb4d2 100644 --- a/src/plugins/update.test.ts +++ b/src/plugins/update.test.ts @@ -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", diff --git a/src/plugins/update.ts b/src/plugins/update.ts index 3d2bf90c2db..a8edb9be9f4 100644 --- a/src/plugins/update.ts +++ b/src/plugins/update.ts @@ -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< diff --git a/test/scripts/docker-build-helper.test.ts b/test/scripts/docker-build-helper.test.ts index e98ad6c59ec..6df92ea56de 100644 --- a/test/scripts/docker-build-helper.test.ts +++ b/test/scripts/docker-build-helper.test.ts @@ -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"); }); }); diff --git a/test/scripts/plugin-prerelease-test-plan.test.ts b/test/scripts/plugin-prerelease-test-plan.test.ts index f2241cb86fb..938c9f68e7a 100644 --- a/test/scripts/plugin-prerelease-test-plan.test.ts +++ b/test/scripts/plugin-prerelease-test-plan.test.ts @@ -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(