mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:10:45 +00:00
fix(plugins): persist clawhub artifact metadata
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
72
src/plugins/clawhub-install-records.ts
Normal file
72
src/plugins/clawhub-install-records.ts
Normal 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 } : {}),
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -49,6 +49,11 @@ export type InstalledPluginInstallRecordInfo = Pick<
|
||||
| "clawhubPackage"
|
||||
| "clawhubFamily"
|
||||
| "clawhubChannel"
|
||||
| "artifactKind"
|
||||
| "artifactFormat"
|
||||
| "npmIntegrity"
|
||||
| "npmShasum"
|
||||
| "npmTarballName"
|
||||
| "clawpackSha256"
|
||||
| "clawpackSpecVersion"
|
||||
| "clawpackManifestSha256"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user