mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:40:44 +00:00
fix(plugins): align clawhub clawpack downloads
This commit is contained in:
@@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- 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.
|
||||
- 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 ClawPack, 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 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,7 +84,7 @@ 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 that artifact, verifies the ClawHub digest header and
|
||||
OpenClaw prefers the exact uploaded npm-pack `.tgz`, verifies the ClawHub digest header and
|
||||
downloaded bytes, and records the ClawPack digest metadata for later
|
||||
updates. Older package versions without ClawPack metadata still use the
|
||||
legacy package archive verification path.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const crypto = require("node:crypto");
|
||||
const fs = require("node:fs");
|
||||
const http = require("node:http");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const { createRequire } = require("node:module");
|
||||
|
||||
@@ -8,18 +9,82 @@ const profile = process.argv[2];
|
||||
const portFile = process.argv[3];
|
||||
const requireFromApp = createRequire(path.join(process.cwd(), "package.json"));
|
||||
const JSZip = requireFromApp("jszip");
|
||||
const tar = requireFromApp("tar");
|
||||
const packageName = "@openclaw/kitchen-sink";
|
||||
const pluginId = "openclaw-kitchen-sink-fixture";
|
||||
|
||||
const buildClawPackSummary = ({ sha256hash, manifestSha256, size }) => ({
|
||||
available: true,
|
||||
specVersion: 1,
|
||||
format: "clawpack.zip",
|
||||
sha256: sha256hash,
|
||||
size,
|
||||
manifestSha256,
|
||||
const buildArtifactSummary = ({
|
||||
clawpackSha256,
|
||||
clawpackSize,
|
||||
npmIntegrity,
|
||||
npmShasum,
|
||||
npmTarballName,
|
||||
}) => ({
|
||||
kind: "npm-pack",
|
||||
format: "tgz",
|
||||
sha256: clawpackSha256,
|
||||
size: clawpackSize,
|
||||
npmIntegrity,
|
||||
npmShasum,
|
||||
npmTarballName,
|
||||
});
|
||||
|
||||
const buildClawPackSummary = ({
|
||||
clawpackSha256,
|
||||
clawpackSize,
|
||||
npmIntegrity,
|
||||
npmShasum,
|
||||
npmTarballName,
|
||||
}) => ({
|
||||
available: true,
|
||||
format: "tgz",
|
||||
sha256: clawpackSha256,
|
||||
size: clawpackSize,
|
||||
npmIntegrity,
|
||||
npmShasum,
|
||||
npmTarballName,
|
||||
});
|
||||
|
||||
async function buildNpmPackArtifact(fixture) {
|
||||
const packRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), "openclaw-clawhub-fixture-"));
|
||||
try {
|
||||
const packageDir = path.join(packRoot, "package");
|
||||
await fs.promises.mkdir(packageDir, { recursive: true });
|
||||
await fs.promises.writeFile(
|
||||
path.join(packageDir, "package.json"),
|
||||
`${JSON.stringify(fixture.packageJson, null, 2)}\n`,
|
||||
);
|
||||
await fs.promises.writeFile(path.join(packageDir, "index.js"), fixture.indexJs);
|
||||
await fs.promises.writeFile(
|
||||
path.join(packageDir, "openclaw.plugin.json"),
|
||||
`${JSON.stringify(fixture.manifest, null, 2)}\n`,
|
||||
);
|
||||
const npmTarballName = `${packageName.replace(/^@/, "").replace("/", "-")}-${fixture.version}.tgz`;
|
||||
const archivePath = path.join(packRoot, npmTarballName);
|
||||
await tar.c(
|
||||
{
|
||||
cwd: packRoot,
|
||||
file: archivePath,
|
||||
gzip: true,
|
||||
portable: true,
|
||||
noMtime: true,
|
||||
},
|
||||
["package"],
|
||||
);
|
||||
const archive = await fs.promises.readFile(archivePath);
|
||||
return {
|
||||
archive,
|
||||
clawpackSha256: crypto.createHash("sha256").update(archive).digest("hex"),
|
||||
clawpackSize: archive.length,
|
||||
npmIntegrity: `sha512-${crypto.createHash("sha512").update(archive).digest("base64")}`,
|
||||
npmShasum: crypto.createHash("sha1").update(archive).digest("hex"),
|
||||
npmTarballName,
|
||||
};
|
||||
} finally {
|
||||
await fs.promises.rm(packRoot, { recursive: true, force: true }).catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
const profiles = {
|
||||
"kitchen-sink-plugin": {
|
||||
version: "0.1.3",
|
||||
@@ -98,6 +163,7 @@ export default definePluginEntry({
|
||||
},
|
||||
packageDetail(artifact) {
|
||||
const clawpack = buildClawPackSummary(artifact);
|
||||
const packageArtifact = buildArtifactSummary(artifact);
|
||||
const packageDetail = {
|
||||
package: {
|
||||
name: packageName,
|
||||
@@ -131,6 +197,7 @@ export default definePluginEntry({
|
||||
hasProvenance: false,
|
||||
scanStatus: "passed",
|
||||
},
|
||||
artifact: packageArtifact,
|
||||
clawpack,
|
||||
},
|
||||
};
|
||||
@@ -151,6 +218,7 @@ export default definePluginEntry({
|
||||
compatibility: packageDetail.package.compatibility,
|
||||
capabilities: packageDetail.package.capabilities,
|
||||
verification: packageDetail.package.verification,
|
||||
artifact: packageArtifact,
|
||||
clawpack,
|
||||
},
|
||||
},
|
||||
@@ -209,6 +277,7 @@ export default definePluginEntry({
|
||||
minGatewayVersion: "2026.4.26",
|
||||
};
|
||||
const clawpack = buildClawPackSummary(artifact);
|
||||
const packageArtifact = buildArtifactSummary(artifact);
|
||||
return {
|
||||
packageDetail: {
|
||||
package: {
|
||||
@@ -222,6 +291,7 @@ export default definePluginEntry({
|
||||
createdAt: 0,
|
||||
updatedAt: 0,
|
||||
compatibility,
|
||||
artifact: packageArtifact,
|
||||
clawpack,
|
||||
},
|
||||
},
|
||||
@@ -232,6 +302,7 @@ export default definePluginEntry({
|
||||
changelog: "Kitchen-sink fixture package for Docker plugin E2E.",
|
||||
sha256hash: artifact.sha256hash,
|
||||
compatibility,
|
||||
artifact: packageArtifact,
|
||||
clawpack,
|
||||
},
|
||||
},
|
||||
@@ -257,11 +328,10 @@ async function main() {
|
||||
|
||||
const archive = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
|
||||
const sha256hash = crypto.createHash("sha256").update(archive).digest("hex");
|
||||
const manifestSha256 = crypto.createHash("sha256").update(manifestJson).digest("hex");
|
||||
const clawpack = await buildNpmPackArtifact(fixture);
|
||||
const { packageDetail, versionDetail, betaStatus } = fixture.packageDetail({
|
||||
sha256hash,
|
||||
manifestSha256,
|
||||
size: archive.length,
|
||||
...clawpack,
|
||||
});
|
||||
|
||||
const json = (response, value, status = 200) => {
|
||||
@@ -304,15 +374,17 @@ async function main() {
|
||||
}
|
||||
if (
|
||||
url.pathname ===
|
||||
`/api/v1/packages/${encodeURIComponent(packageName)}/versions/${fixture.version}/clawpack`
|
||||
`/api/v1/packages/${encodeURIComponent(packageName)}/versions/${fixture.version}/artifact/download`
|
||||
) {
|
||||
response.writeHead(200, {
|
||||
"content-type": "application/zip",
|
||||
"content-length": String(archive.length),
|
||||
"X-ClawHub-ClawPack-Sha256": sha256hash,
|
||||
"X-ClawHub-ClawPack-Spec-Version": "1",
|
||||
"content-type": "application/octet-stream",
|
||||
"content-length": String(clawpack.archive.length),
|
||||
"X-ClawHub-Artifact-Type": "npm-pack-tarball",
|
||||
"X-ClawHub-Artifact-Sha256": clawpack.clawpackSha256,
|
||||
"X-ClawHub-Npm-Integrity": clawpack.npmIntegrity,
|
||||
"X-ClawHub-Npm-Shasum": clawpack.npmShasum,
|
||||
});
|
||||
response.end(archive);
|
||||
response.end(clawpack.archive);
|
||||
return;
|
||||
}
|
||||
response.writeHead(404, { "content-type": "text/plain" });
|
||||
|
||||
@@ -389,12 +389,7 @@ function assertInstalled() {
|
||||
if (!record.version || !record.integrity || !record.resolvedAt) {
|
||||
throw new Error(`missing ClawHub resolution metadata: ${JSON.stringify(record)}`);
|
||||
}
|
||||
if (
|
||||
!record.clawpackSha256 ||
|
||||
record.clawpackSpecVersion !== 1 ||
|
||||
!record.clawpackManifestSha256 ||
|
||||
typeof record.clawpackSize !== "number"
|
||||
) {
|
||||
if (!record.clawpackSha256 || typeof record.clawpackSize !== "number") {
|
||||
throw new Error(`missing kitchen-sink ClawPack metadata: ${JSON.stringify(record)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,12 +534,7 @@ function assertClawHubInstalled() {
|
||||
if (typeof record.installPath !== "string" || record.installPath.length === 0) {
|
||||
throw new Error(`missing ClawHub install path for ${pluginId}`);
|
||||
}
|
||||
if (
|
||||
!record.clawpackSha256 ||
|
||||
record.clawpackSpecVersion !== 1 ||
|
||||
!record.clawpackManifestSha256 ||
|
||||
typeof record.clawpackSize !== "number"
|
||||
) {
|
||||
if (!record.clawpackSha256 || typeof record.clawpackSize !== "number") {
|
||||
throw new Error(`missing ClawHub ClawPack metadata for ${pluginId}: ${JSON.stringify(record)}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -257,21 +257,22 @@ describe("clawhub helpers", () => {
|
||||
return new Response(bytes, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/zip",
|
||||
"X-ClawHub-ClawPack-Sha256": sha256Hex,
|
||||
"X-ClawHub-ClawPack-Spec-Version": "1",
|
||||
"content-type": "application/octet-stream",
|
||||
"X-ClawHub-Artifact-Sha256": sha256Hex,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
expect(new URL(requestedUrl).pathname).toBe("/api/v1/packages/demo/versions/1.2.3/clawpack");
|
||||
expect(path.basename(archive.archivePath)).toBe("demo.clawpack.zip");
|
||||
expect(new URL(requestedUrl).pathname).toBe(
|
||||
"/api/v1/packages/demo/versions/1.2.3/artifact/download",
|
||||
);
|
||||
expect(path.basename(archive.archivePath)).toBe("demo-1.2.3.tgz");
|
||||
expect(archive.artifact).toBe("clawpack");
|
||||
expect(archive.sha256Hex).toBe(sha256Hex);
|
||||
expect(archive.clawpackHeaderSha256).toBe(sha256Hex);
|
||||
expect(archive.clawpackHeaderSpecVersion).toBe(1);
|
||||
expect(archive.npmIntegrity).toMatch(/^sha512-/);
|
||||
await expect(fs.readFile(archive.archivePath)).resolves.toEqual(Buffer.from(bytes));
|
||||
} finally {
|
||||
const archiveDir = path.dirname(archive.archivePath);
|
||||
@@ -290,8 +291,8 @@ describe("clawhub helpers", () => {
|
||||
new Response(new Uint8Array([7, 8, 9]), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/zip",
|
||||
"X-ClawHub-ClawPack-Sha256": "0".repeat(64),
|
||||
"content-type": "application/octet-stream",
|
||||
"X-ClawHub-Artifact-Sha256": "0".repeat(64),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -39,6 +39,20 @@ export type ClawHubPackageEnvironmentSummary = {
|
||||
supportsRemoteHost?: boolean;
|
||||
knownUnsupported?: string[];
|
||||
};
|
||||
export type ClawHubPackageArtifactSummary = {
|
||||
kind?: "legacy-zip" | "npm-pack" | null;
|
||||
sha256?: string | null;
|
||||
size?: number | null;
|
||||
format?: "zip" | "tgz" | null;
|
||||
npmIntegrity?: string | null;
|
||||
npmShasum?: string | null;
|
||||
npmTarballName?: string | null;
|
||||
npmUnpackedSize?: number | null;
|
||||
npmFileCount?: number | null;
|
||||
downloadUrl?: string | null;
|
||||
tarballUrl?: string | null;
|
||||
legacyDownloadUrl?: string | null;
|
||||
};
|
||||
export type ClawHubPackageClawPackSummary = {
|
||||
available: boolean;
|
||||
specVersion?: number | null;
|
||||
@@ -47,6 +61,9 @@ export type ClawHubPackageClawPackSummary = {
|
||||
size?: number | null;
|
||||
fileCount?: number | null;
|
||||
manifestSha256?: string | null;
|
||||
npmIntegrity?: string | null;
|
||||
npmShasum?: string | null;
|
||||
npmTarballName?: string | null;
|
||||
builtAt?: number | null;
|
||||
buildVersion?: string | null;
|
||||
hostTargets?: ClawHubPackageHostTarget[];
|
||||
@@ -71,6 +88,7 @@ export type ClawHubPackageListItem = {
|
||||
clawpackAvailable?: boolean;
|
||||
hostTargetKeys?: string[];
|
||||
environmentFlags?: string[];
|
||||
artifact?: ClawHubPackageArtifactSummary | null;
|
||||
clawpack?: ClawHubPackageClawPackSummary;
|
||||
};
|
||||
export type ClawHubPackageDetail = {
|
||||
@@ -99,6 +117,7 @@ export type ClawHubPackageDetail = {
|
||||
hasProvenance?: boolean;
|
||||
scanStatus?: string;
|
||||
} | null;
|
||||
artifact?: ClawHubPackageArtifactSummary | null;
|
||||
clawpack?: ClawHubPackageClawPackSummary;
|
||||
})
|
||||
| null;
|
||||
@@ -138,6 +157,7 @@ export type ClawHubPackageVersion = {
|
||||
? C
|
||||
: never
|
||||
: never;
|
||||
artifact?: ClawHubPackageArtifactSummary | null;
|
||||
clawpack?: ClawHubPackageClawPackSummary;
|
||||
} | null;
|
||||
};
|
||||
@@ -209,6 +229,9 @@ export type ClawHubDownloadResult = {
|
||||
artifact: "archive" | "clawpack";
|
||||
clawpackHeaderSha256?: string;
|
||||
clawpackHeaderSpecVersion?: number;
|
||||
npmIntegrity?: string;
|
||||
npmShasum?: string;
|
||||
npmTarballName?: string;
|
||||
cleanup: () => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -476,6 +499,24 @@ function formatSha256Hex(bytes: Uint8Array): string {
|
||||
return createHash("sha256").update(bytes).digest("hex");
|
||||
}
|
||||
|
||||
function formatSha512Integrity(bytes: Uint8Array): string {
|
||||
const digest = createHash("sha512").update(bytes).digest("base64");
|
||||
return `sha512-${digest}`;
|
||||
}
|
||||
|
||||
function normalizeHeaderValue(value: string | null): string | undefined {
|
||||
const normalized = normalizeOptionalString(value);
|
||||
return normalized && normalized.length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
function safePackageTarballName(name: string, version: string): string {
|
||||
const base = name
|
||||
.replace(/^@/, "")
|
||||
.replace(/[\\/]+/g, "-")
|
||||
.replace(/[^A-Za-z0-9._-]/g, "-");
|
||||
return `${base || "package"}-${version}.tgz`;
|
||||
}
|
||||
|
||||
export function normalizeClawHubSha256Integrity(value: string): string | null {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
@@ -645,7 +686,7 @@ export async function downloadClawHubPackageArchive(params: {
|
||||
baseUrl: params.baseUrl,
|
||||
path: `/api/v1/packages/${encodeURIComponent(params.name)}/versions/${encodeURIComponent(
|
||||
params.version,
|
||||
)}/clawpack`,
|
||||
)}/artifact/download`,
|
||||
token: params.token,
|
||||
timeoutMs: params.timeoutMs,
|
||||
fetchImpl: params.fetchImpl,
|
||||
@@ -659,12 +700,15 @@ export async function downloadClawHubPackageArchive(params: {
|
||||
}
|
||||
const bytes = new Uint8Array(await response.arrayBuffer());
|
||||
const sha256Hex = formatSha256Hex(bytes);
|
||||
const npmIntegrity = formatSha512Integrity(bytes);
|
||||
const headerSha256 = normalizeClawHubSha256Hex(
|
||||
response.headers.get("X-ClawHub-ClawPack-Sha256") ?? "",
|
||||
response.headers.get("X-ClawHub-Artifact-Sha256") ??
|
||||
response.headers.get("X-ClawHub-ClawPack-Sha256") ??
|
||||
"",
|
||||
);
|
||||
if (!headerSha256) {
|
||||
throw new Error(
|
||||
`ClawHub ClawPack download for "${params.name}@${params.version}" is missing X-ClawHub-ClawPack-Sha256.`,
|
||||
`ClawHub ClawPack download for "${params.name}@${params.version}" is missing X-ClawHub-Artifact-Sha256.`,
|
||||
);
|
||||
}
|
||||
if (headerSha256 !== sha256Hex) {
|
||||
@@ -672,11 +716,22 @@ export async function downloadClawHubPackageArchive(params: {
|
||||
`ClawHub ClawPack download for "${params.name}@${params.version}" declared sha256 ${headerSha256}, got ${sha256Hex}.`,
|
||||
);
|
||||
}
|
||||
const headerNpmIntegrity = normalizeHeaderValue(
|
||||
response.headers.get("X-ClawHub-Npm-Integrity"),
|
||||
);
|
||||
if (headerNpmIntegrity && headerNpmIntegrity !== npmIntegrity) {
|
||||
throw new Error(
|
||||
`ClawHub ClawPack download for "${params.name}@${params.version}" declared npm integrity ${headerNpmIntegrity}, got ${npmIntegrity}.`,
|
||||
);
|
||||
}
|
||||
const npmTarballName =
|
||||
normalizeHeaderValue(response.headers.get("X-ClawHub-Npm-Tarball-Name")) ??
|
||||
safePackageTarballName(params.name, params.version);
|
||||
const rawSpecVersion = response.headers.get("X-ClawHub-ClawPack-Spec-Version");
|
||||
const specVersion = rawSpecVersion ? Number.parseInt(rawSpecVersion, 10) : undefined;
|
||||
const target = await createTempDownloadTarget({
|
||||
prefix: "openclaw-clawhub-clawpack",
|
||||
fileName: `${params.name}.clawpack.zip`,
|
||||
fileName: npmTarballName,
|
||||
tmpDir: os.tmpdir(),
|
||||
});
|
||||
await fs.writeFile(target.path, bytes);
|
||||
@@ -689,6 +744,11 @@ export async function downloadClawHubPackageArchive(params: {
|
||||
...(typeof specVersion === "number" && Number.isSafeInteger(specVersion) && specVersion >= 0
|
||||
? { clawpackHeaderSpecVersion: specVersion }
|
||||
: {}),
|
||||
npmIntegrity,
|
||||
...(normalizeHeaderValue(response.headers.get("X-ClawHub-Npm-Shasum"))
|
||||
? { npmShasum: normalizeHeaderValue(response.headers.get("X-ClawHub-Npm-Shasum")) }
|
||||
: {}),
|
||||
npmTarballName,
|
||||
cleanup: target.cleanup,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -57,8 +57,6 @@ const DEMO_CLAWPACK_SHA256 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
const DEMO_CLAWPACK_INTEGRITY = `sha256-${Buffer.from(DEMO_CLAWPACK_SHA256, "hex").toString(
|
||||
"base64",
|
||||
)}`;
|
||||
const DEMO_CLAWPACK_MANIFEST_SHA256 =
|
||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
function sha256Hex(value: string): string {
|
||||
@@ -336,29 +334,24 @@ describe("installPluginFromClawHub", () => {
|
||||
pluginApiRange: ">=2026.3.22",
|
||||
minGatewayVersion: "2026.3.0",
|
||||
},
|
||||
clawpack: {
|
||||
available: true,
|
||||
specVersion: 1,
|
||||
format: "clawpack.zip",
|
||||
artifact: {
|
||||
kind: "npm-pack",
|
||||
format: "tgz",
|
||||
sha256: DEMO_CLAWPACK_SHA256,
|
||||
size: 4096,
|
||||
fileCount: 7,
|
||||
manifestSha256: DEMO_CLAWPACK_MANIFEST_SHA256,
|
||||
builtAt: 1774200000000,
|
||||
buildVersion: "2026.3.22",
|
||||
hostTargets: [],
|
||||
environment: null,
|
||||
runtimeBundles: [],
|
||||
npmIntegrity: "sha512-clawpack",
|
||||
npmShasum: "1".repeat(40),
|
||||
npmTarballName: "demo-2026.3.22.tgz",
|
||||
},
|
||||
},
|
||||
});
|
||||
downloadClawHubPackageArchiveMock.mockResolvedValueOnce({
|
||||
archivePath: "/tmp/clawhub-demo/clawpack.zip",
|
||||
archivePath: "/tmp/clawhub-demo/demo-2026.3.22.tgz",
|
||||
integrity: DEMO_CLAWPACK_INTEGRITY,
|
||||
sha256Hex: DEMO_CLAWPACK_SHA256,
|
||||
artifact: "clawpack",
|
||||
clawpackHeaderSha256: DEMO_CLAWPACK_SHA256,
|
||||
clawpackHeaderSpecVersion: 1,
|
||||
npmIntegrity: "sha512-clawpack",
|
||||
cleanup: archiveCleanupMock,
|
||||
});
|
||||
|
||||
@@ -372,8 +365,6 @@ describe("installPluginFromClawHub", () => {
|
||||
clawhub: {
|
||||
integrity: DEMO_CLAWPACK_INTEGRITY,
|
||||
clawpackSha256: DEMO_CLAWPACK_SHA256,
|
||||
clawpackSpecVersion: 1,
|
||||
clawpackManifestSha256: DEMO_CLAWPACK_MANIFEST_SHA256,
|
||||
clawpackSize: 4096,
|
||||
},
|
||||
});
|
||||
@@ -396,23 +387,20 @@ describe("installPluginFromClawHub", () => {
|
||||
pluginApiRange: ">=2026.3.22",
|
||||
minGatewayVersion: "2026.3.0",
|
||||
},
|
||||
clawpack: {
|
||||
available: true,
|
||||
specVersion: 1,
|
||||
format: "clawpack.zip",
|
||||
artifact: {
|
||||
kind: "npm-pack",
|
||||
format: "tgz",
|
||||
sha256: DEMO_CLAWPACK_SHA256,
|
||||
size: 4096,
|
||||
manifestSha256: DEMO_CLAWPACK_MANIFEST_SHA256,
|
||||
},
|
||||
},
|
||||
});
|
||||
downloadClawHubPackageArchiveMock.mockResolvedValueOnce({
|
||||
archivePath: "/tmp/clawhub-demo/clawpack.zip",
|
||||
archivePath: "/tmp/clawhub-demo/demo-2026.3.22.tgz",
|
||||
integrity: DEMO_CLAWPACK_INTEGRITY,
|
||||
sha256Hex: DEMO_CLAWPACK_SHA256,
|
||||
artifact: "clawpack",
|
||||
clawpackHeaderSha256: DEMO_CLAWPACK_SHA256,
|
||||
clawpackHeaderSpecVersion: 1,
|
||||
cleanup: archiveCleanupMock,
|
||||
});
|
||||
|
||||
@@ -435,7 +423,7 @@ describe("installPluginFromClawHub", () => {
|
||||
);
|
||||
expect(installPluginFromArchiveMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
archivePath: "/tmp/clawhub-demo/clawpack.zip",
|
||||
archivePath: "/tmp/clawhub-demo/demo-2026.3.22.tgz",
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -451,16 +439,15 @@ describe("installPluginFromClawHub", () => {
|
||||
pluginApiRange: ">=2026.3.22",
|
||||
minGatewayVersion: "2026.3.0",
|
||||
},
|
||||
clawpack: {
|
||||
available: true,
|
||||
specVersion: 1,
|
||||
format: "clawpack.zip",
|
||||
artifact: {
|
||||
kind: "npm-pack",
|
||||
format: "tgz",
|
||||
sha256: DEMO_CLAWPACK_SHA256,
|
||||
},
|
||||
},
|
||||
});
|
||||
downloadClawHubPackageArchiveMock.mockResolvedValueOnce({
|
||||
archivePath: "/tmp/clawhub-demo/clawpack.zip",
|
||||
archivePath: "/tmp/clawhub-demo/demo-2026.3.22.tgz",
|
||||
integrity: `sha256-${Buffer.from(mismatchedSha256, "hex").toString("base64")}`,
|
||||
sha256Hex: mismatchedSha256,
|
||||
artifact: "clawpack",
|
||||
@@ -497,19 +484,11 @@ describe("installPluginFromClawHub", () => {
|
||||
pluginApiRange: ">=2026.3.22",
|
||||
minGatewayVersion: "2026.3.0",
|
||||
},
|
||||
clawpack: {
|
||||
available: true,
|
||||
specVersion: 1,
|
||||
format: "clawpack.zip",
|
||||
artifact: {
|
||||
kind: "npm-pack",
|
||||
format: "tgz",
|
||||
sha256: DEMO_CLAWPACK_SHA256,
|
||||
size: 4096,
|
||||
fileCount: 7,
|
||||
manifestSha256: DEMO_CLAWPACK_MANIFEST_SHA256,
|
||||
builtAt: 1774200000000,
|
||||
buildVersion: "2026.3.22",
|
||||
hostTargets: [],
|
||||
environment: null,
|
||||
runtimeBundles: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
satisfiesGatewayMinimum,
|
||||
satisfiesPluginApiRange,
|
||||
type ClawHubPackageChannel,
|
||||
type ClawHubPackageArtifactSummary,
|
||||
type ClawHubPackageCompatibility,
|
||||
type ClawHubPackageDetail,
|
||||
type ClawHubPackageFamily,
|
||||
@@ -128,22 +129,26 @@ type ClawHubArchiveEntryLimits = {
|
||||
};
|
||||
|
||||
function normalizeClawHubClawPackInstallFields(
|
||||
clawpack: ClawHubPackageClawPackSummary | null | undefined,
|
||||
clawpack: ClawHubPackageArtifactSummary | ClawHubPackageClawPackSummary | null | undefined,
|
||||
): Pick<
|
||||
ClawHubPluginInstallRecordFields,
|
||||
"clawpackSha256" | "clawpackSpecVersion" | "clawpackManifestSha256" | "clawpackSize"
|
||||
> {
|
||||
if (clawpack?.available !== true) {
|
||||
const isNpmPackArtifact =
|
||||
clawpack && "kind" in clawpack && normalizeOptionalString(clawpack.kind) === "npm-pack";
|
||||
const isLegacyClawPack = clawpack && "available" in clawpack && clawpack.available;
|
||||
if (!isNpmPackArtifact && !isLegacyClawPack) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const clawpackSha256 =
|
||||
typeof clawpack.sha256 === "string" ? normalizeClawHubSha256Hex(clawpack.sha256) : null;
|
||||
const clawpackManifestSha256 =
|
||||
typeof clawpack.manifestSha256 === "string"
|
||||
"manifestSha256" in clawpack && typeof clawpack.manifestSha256 === "string"
|
||||
? normalizeClawHubSha256Hex(clawpack.manifestSha256)
|
||||
: null;
|
||||
const clawpackSpecVersion =
|
||||
"specVersion" in clawpack &&
|
||||
typeof clawpack.specVersion === "number" &&
|
||||
Number.isSafeInteger(clawpack.specVersion) &&
|
||||
clawpack.specVersion >= 0
|
||||
@@ -174,14 +179,35 @@ function isTrustedSourceLinkedOfficialPackage(pkg: NonNullable<ClawHubPackageDet
|
||||
}
|
||||
|
||||
function resolveClawHubClawPackArtifactSha256(
|
||||
clawpack: ClawHubPackageClawPackSummary | null | undefined,
|
||||
clawpack: ClawHubPackageArtifactSummary | ClawHubPackageClawPackSummary | null | undefined,
|
||||
): string | null {
|
||||
if (clawpack?.available !== true || typeof clawpack.sha256 !== "string") {
|
||||
const isNpmPackArtifact =
|
||||
clawpack && "kind" in clawpack && normalizeOptionalString(clawpack.kind) === "npm-pack";
|
||||
const isLegacyClawPack = clawpack && "available" in clawpack && clawpack.available;
|
||||
if ((!isNpmPackArtifact && !isLegacyClawPack) || typeof clawpack.sha256 !== "string") {
|
||||
return null;
|
||||
}
|
||||
return normalizeClawHubSha256Hex(clawpack.sha256);
|
||||
}
|
||||
|
||||
function resolveClawHubNpmIntegrity(
|
||||
clawpack: ClawHubPackageArtifactSummary | ClawHubPackageClawPackSummary | null | undefined,
|
||||
): string | null {
|
||||
return normalizeOptionalString(clawpack?.npmIntegrity) ?? null;
|
||||
}
|
||||
|
||||
function resolveClawHubNpmPackArtifact(
|
||||
version: NonNullable<ClawHubPackageVersion["version"]>,
|
||||
): ClawHubPackageArtifactSummary | ClawHubPackageClawPackSummary | null {
|
||||
if (version.artifact?.kind === "npm-pack") {
|
||||
return version.artifact;
|
||||
}
|
||||
if (version.clawpack?.available === true) {
|
||||
return version.clawpack;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function formatClawHubSpecifier(params: { name: string; version?: string }): string {
|
||||
return `clawhub:${params.name}${params.version ? `@${params.version}` : ""}`;
|
||||
}
|
||||
@@ -661,7 +687,7 @@ async function resolveCompatiblePackageVersion(params: {
|
||||
version: string;
|
||||
compatibility?: ClawHubPackageCompatibility | null;
|
||||
verification: ClawHubArchiveVerification | null;
|
||||
clawpack?: ClawHubPackageClawPackSummary | null;
|
||||
clawpack?: ClawHubPackageArtifactSummary | ClawHubPackageClawPackSummary | null;
|
||||
}
|
||||
| ClawHubInstallFailure
|
||||
> {
|
||||
@@ -699,7 +725,9 @@ async function resolveCompatiblePackageVersion(params: {
|
||||
clawpack: versionDetail.version?.clawpack ?? null,
|
||||
};
|
||||
}
|
||||
const clawpack = versionDetail.version?.clawpack ?? null;
|
||||
const clawpack = versionDetail.version
|
||||
? resolveClawHubNpmPackArtifact(versionDetail.version)
|
||||
: null;
|
||||
const verificationState = resolveClawHubArchiveVerification(
|
||||
versionDetail,
|
||||
params.detail.package?.name ?? "unknown",
|
||||
@@ -910,6 +938,7 @@ export async function installPluginFromClawHub(
|
||||
try {
|
||||
if (expectedClawPackSha256) {
|
||||
const expectedIntegrity = normalizeClawHubSha256Integrity(expectedClawPackSha256);
|
||||
const expectedNpmIntegrity = resolveClawHubNpmIntegrity(versionState.clawpack);
|
||||
if (
|
||||
archive.artifact !== "clawpack" ||
|
||||
archive.clawpackHeaderSha256 !== expectedClawPackSha256 ||
|
||||
@@ -921,6 +950,12 @@ export async function installPluginFromClawHub(
|
||||
CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH,
|
||||
);
|
||||
}
|
||||
if (expectedNpmIntegrity && archive.npmIntegrity !== expectedNpmIntegrity) {
|
||||
return buildClawHubInstallFailure(
|
||||
`ClawHub ClawPack npm integrity mismatch for "${parsed.name}@${versionState.version}": expected ${expectedNpmIntegrity}, got ${archive.npmIntegrity ?? "unknown"}.`,
|
||||
CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH,
|
||||
);
|
||||
}
|
||||
} else if (versionState.verification?.kind === "archive-integrity") {
|
||||
if (archive.integrity !== versionState.verification.integrity) {
|
||||
return buildClawHubInstallFailure(
|
||||
|
||||
@@ -142,7 +142,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => {
|
||||
'from "openclaw/plugin-sdk/plugin-entry"',
|
||||
);
|
||||
expect(readFileSync("scripts/e2e/lib/clawhub-fixture-server.cjs", "utf8")).toContain(
|
||||
"X-ClawHub-ClawPack-Sha256",
|
||||
"X-ClawHub-Artifact-Sha256",
|
||||
);
|
||||
expect(script).toContain("docker stats --no-stream");
|
||||
expect(sweepScript).toContain("scan_logs_for_unexpected_errors");
|
||||
|
||||
Reference in New Issue
Block a user