Files
openclaw/test/scripts/clawhub-bootstrap-artifact.test.ts
Vincent Koc 54cc90b1b1 fix(release): publish ClawHub bootstrap artifacts immutably (#103809)
* fix(release): harden ClawHub bootstrap

* fix(release): centralize publication artifact verification

* fix(release): harden publication artifact verification

* fix(release): lock ClawHub bootstrap toolchain

* test(release): assert locked ClawHub binary

* fix(release): pack with trusted ClawHub harness

* fix(release): bound beta verifier artifact reads

* fix(release): bind target and tooling identities

* fix(release): stabilize signed package ordering

* fix(release): bind ClawHub pretag proof

* fix(release): bind ClawHub OIDC readback

* fix(release): bind ClawHub OIDC readback
2026-07-10 13:33:20 -07:00

538 lines
16 KiB
TypeScript

import { createHash } from "node:crypto";
import {
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
symlinkSync,
truncateSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { gzipSync } from "node:zlib";
import { afterEach, describe, expect, it } from "vitest";
import {
createClawHubBootstrapArtifactManifest,
downloadClawHubBootstrapArtifact,
verifyClawHubPackedArtifactIdentity,
verifyClawHubBootstrapArtifactManifest,
} from "../../scripts/lib/clawhub-bootstrap-artifact.mjs";
const tempDirs: string[] = [];
const targetSha = "a".repeat(40);
const workflowSha = "b".repeat(40);
const clawhubToolchainSha256 = "c".repeat(64);
const clawhubToolchainVersion = "0.23.1";
const clawhubToolchainIntegrity =
"sha512-YvUImhsVaM90BUAv3uP7lfABziwR5XL3ch2Owa+GvNxwQ2xzZFmZC0yVjAtQbvep+dDDS16nUGRwKx7jqnTOEA==";
afterEach(() => {
for (const directory of tempDirs.splice(0)) {
rmSync(directory, { recursive: true, force: true });
}
});
function fixture() {
const root = mkdtempSync(join(tmpdir(), "openclaw-clawhub-bootstrap-"));
tempDirs.push(root);
const artifactRoot = join(root, "artifact");
const packageRoot = join(artifactRoot, "packages", "meta");
const existingPackageRoot = join(artifactRoot, "packages", "existing");
mkdirSync(packageRoot, { recursive: true });
mkdirSync(existingPackageRoot, { recursive: true });
writeFileSync(join(packageRoot, "openclaw-meta-2026.7.1-beta.3.tgz"), "packed meta");
writeFileSync(
join(existingPackageRoot, "openclaw-existing-2026.7.1-beta.3.tgz"),
"packed existing",
);
const matrixPath = join(root, "matrix.json");
writeFileSync(
matrixPath,
JSON.stringify([
{
packageName: "@openclaw/meta",
version: "2026.7.1-beta.3",
packageDir: "extensions/meta",
publishTag: "beta",
bootstrapMode: "publish",
requiresManualOverride: false,
},
{
packageName: "@openclaw/existing",
version: "2026.7.1-beta.3",
packageDir: "extensions/existing",
publishTag: "beta",
bootstrapMode: "configure-only",
requiresManualOverride: true,
},
]),
);
return {
artifactRoot,
matrixPath,
manifestPath: join(artifactRoot, "manifest.json"),
};
}
function common(paths: ReturnType<typeof fixture>) {
return {
artifactRoot: paths.artifactRoot,
artifactName: `clawhub-bootstrap-${targetSha.slice(0, 12)}-123-2`,
clawhubToolchainIntegrity,
clawhubToolchainSha256,
clawhubToolchainVersion,
plugins: "@openclaw/meta,@openclaw/existing",
repository: "openclaw/openclaw",
runAttempt: "2",
runId: "123",
targetSha,
workflowSha,
};
}
function writeTarField(header: Buffer, offset: number, length: number, value: string) {
const bytes = Buffer.from(value);
if (bytes.byteLength > length) {
throw new Error(`tar field exceeds ${length} bytes`);
}
bytes.copy(header, offset);
}
function writeTarOctal(header: Buffer, offset: number, length: number, value: number) {
writeTarField(header, offset, length, `${value.toString(8).padStart(length - 2, "0")} \0`);
}
function tarEntry(
name: string,
prefix: string,
contents: string | Uint8Array,
type: "0" | "5" = "0",
) {
const bytes = Buffer.from(contents);
const header = Buffer.alloc(512);
writeTarField(header, 0, 100, name);
writeTarOctal(header, 100, 8, type === "5" ? 0o755 : 0o644);
writeTarOctal(header, 108, 8, 0);
writeTarOctal(header, 116, 8, 0);
writeTarOctal(header, 124, 12, bytes.byteLength);
writeTarOctal(header, 136, 12, 0);
header.fill(0x20, 148, 156);
header[156] = type.charCodeAt(0);
writeTarField(header, 257, 6, "ustar\0");
writeTarField(header, 263, 2, "00");
writeTarOctal(header, 329, 8, 0);
writeTarOctal(header, 337, 8, 0);
writeTarField(header, 345, 155, prefix);
let checksum = 0;
for (const byte of header) {
checksum += byte;
}
writeTarOctal(header, 148, 8, checksum);
const padding = Buffer.alloc((512 - (bytes.byteLength % 512)) % 512);
return Buffer.concat([header, bytes, padding]);
}
function writeClawPack(
entries: Array<{
name: string;
prefix?: string;
contents: string | Uint8Array;
type?: "0" | "5";
}>,
) {
const root = mkdtempSync(join(tmpdir(), "openclaw-clawhub-packed-"));
tempDirs.push(root);
const bytes = gzipSync(
Buffer.concat([
...entries.map((entry) =>
tarEntry(entry.name, entry.prefix ?? "", entry.contents, entry.type),
),
Buffer.alloc(1024),
]),
);
const artifactPath = join(root, "package.tgz");
writeFileSync(artifactPath, bytes);
return {
artifactPath,
bytes,
sha256: createHash("sha256").update(bytes).digest("hex"),
};
}
describe("ClawHub bootstrap artifact manifest", () => {
it("binds the exact package set and packed file identity", async () => {
const paths = fixture();
const created = await createClawHubBootstrapArtifactManifest({
...common(paths),
matrixPath: paths.matrixPath,
outputPath: paths.manifestPath,
});
const meta = created.entries.find((entry) => entry.packageName === "@openclaw/meta");
expect(meta).toMatchObject({
artifactPath: "packages/meta/openclaw-meta-2026.7.1-beta.3.tgz",
size: 11,
});
expect(meta?.sha256).toMatch(/^[a-f0-9]{64}$/u);
expect(created.clawhubToolchainIntegrity).toBe(clawhubToolchainIntegrity);
expect(created.clawhubToolchainSha256).toBe(clawhubToolchainSha256);
expect(created.clawhubToolchainVersion).toBe(clawhubToolchainVersion);
await expect(
verifyClawHubBootstrapArtifactManifest({
...common(paths),
manifestPath: paths.manifestPath,
}),
).resolves.toEqual(created);
});
it("rejects changed bytes and extra artifact files", async () => {
const paths = fixture();
await createClawHubBootstrapArtifactManifest({
...common(paths),
matrixPath: paths.matrixPath,
outputPath: paths.manifestPath,
});
writeFileSync(
join(paths.artifactRoot, "packages", "meta", "openclaw-meta-2026.7.1-beta.3.tgz"),
"changed",
);
await expect(
verifyClawHubBootstrapArtifactManifest({
...common(paths),
manifestPath: paths.manifestPath,
}),
).rejects.toThrow("packed artifact hash or size mismatch");
const manifest = JSON.parse(readFileSync(paths.manifestPath, "utf8"));
writeFileSync(
join(paths.artifactRoot, "packages", "meta", "openclaw-meta-2026.7.1-beta.3.tgz"),
"packed meta",
);
writeFileSync(join(paths.artifactRoot, "unexpected.txt"), "unexpected");
await expect(
verifyClawHubBootstrapArtifactManifest({
...common(paths),
manifestPath: paths.manifestPath,
}),
).rejects.toThrow("artifact inventory mismatch");
expect(manifest.entries).toHaveLength(2);
});
it("uses one exact fatal-UTF8 manifest schema for local and archive consumers", async () => {
const paths = fixture();
const created = await createClawHubBootstrapArtifactManifest({
...common(paths),
matrixPath: paths.matrixPath,
outputPath: paths.manifestPath,
});
writeFileSync(paths.manifestPath, JSON.stringify({ ...created, unexpected: true }));
await expect(
verifyClawHubBootstrapArtifactManifest({
...common(paths),
manifestPath: paths.manifestPath,
}),
).rejects.toThrow("keys are invalid");
writeFileSync(paths.manifestPath, Buffer.from([0xff]));
await expect(
verifyClawHubBootstrapArtifactManifest({
...common(paths),
manifestPath: paths.manifestPath,
}),
).rejects.toThrow("not valid UTF-8 JSON");
writeFileSync(paths.manifestPath, JSON.stringify(created));
await expect(
verifyClawHubBootstrapArtifactManifest({
...common(paths),
clawhubToolchainSha256: "d".repeat(64),
manifestPath: paths.manifestPath,
}),
).rejects.toThrow("clawhubToolchainSha256 mismatch");
writeFileSync(
paths.manifestPath,
JSON.stringify({
...created,
entries: [null, ...created.entries.slice(1)],
}),
);
await expect(
verifyClawHubBootstrapArtifactManifest({
...common(paths),
manifestPath: paths.manifestPath,
}),
).rejects.toThrow("manifest.entries[0] must be an object");
});
it("binds exact target bytes to configure-only repairs", async () => {
const paths = fixture();
const manifest = await createClawHubBootstrapArtifactManifest({
...common(paths),
matrixPath: paths.matrixPath,
outputPath: paths.manifestPath,
});
const existing = manifest.entries.find((entry) => entry.packageName === "@openclaw/existing");
expect(existing).toMatchObject({
artifactPath: "packages/existing/openclaw-existing-2026.7.1-beta.3.tgz",
size: 15,
});
expect(existing?.sha256).toMatch(/^[a-f0-9]{64}$/u);
});
it("rejects preexisting and symlinked download output roots before fetching", async () => {
const paths = fixture();
const downloadOptions = {
artifactDigest: "d".repeat(64),
artifactId: "456",
artifactName: `clawhub-bootstrap-${targetSha.slice(0, 12)}-123-2`,
artifactSize: "1",
clawhubToolchainIntegrity,
clawhubToolchainSha256,
clawhubToolchainVersion,
consumerRunAttempt: "2",
repository: "openclaw/openclaw",
producerJobName: "Pack immutable ClawHub bootstrap artifacts",
runAttempt: "2",
runId: "123",
targetSha,
token: "test-token",
workflowSha,
fetchImpl: (() => {
throw new Error("fetch must not run");
}) as typeof fetch,
};
const existingRoot = join(paths.artifactRoot, "existing-output");
mkdirSync(existingRoot);
await expect(
downloadClawHubBootstrapArtifact({
...downloadOptions,
outputRoot: existingRoot,
}),
).rejects.toThrow("output directory must not already exist");
const symlinkTarget = join(paths.artifactRoot, "symlink-target");
const symlinkRoot = join(paths.artifactRoot, "symlink-output");
mkdirSync(symlinkTarget);
symlinkSync(symlinkTarget, symlinkRoot);
await expect(
downloadClawHubBootstrapArtifact({
...downloadOptions,
outputRoot: symlinkRoot,
}),
).rejects.toThrow("output directory must not already exist");
});
});
describe("ClawHub packed artifact identity", () => {
const expectedIdentity = {
expectedDir: "extensions/meta",
expectedName: "@openclaw/meta-provider",
expectedVersion: "2026.7.1-beta.3",
};
it("accepts one canonical package identity and plugin manifest", async () => {
const pack = writeClawPack([
{
name: "package.json",
prefix: "package",
contents: JSON.stringify({
name: "@openclaw/meta-provider",
version: "2026.7.1-beta.3",
openclaw: {
release: {
publishToClawHub: true,
publishToNpm: true,
},
},
}),
},
{
name: "openclaw.plugin.json",
prefix: "package",
contents: JSON.stringify({ id: "meta" }),
},
]);
await expect(
verifyClawHubPackedArtifactIdentity({
artifactPath: pack.artifactPath,
expectedSha256: pack.sha256,
expectedSize: String(pack.bytes.byteLength),
expectedDir: "extensions/meta",
expectedName: "@openclaw/meta-provider",
expectedVersion: "2026.7.1-beta.3",
}),
).resolves.toMatchObject({
packageName: "@openclaw/meta-provider",
packageVersion: "2026.7.1-beta.3",
sha256: pack.sha256,
size: pack.bytes.byteLength,
});
});
it("rejects a whitespace-bearing alias before a later package.json", async () => {
const pack = writeClawPack([
{
name: " package.json ",
prefix: " package ",
contents: JSON.stringify({
name: "@openclaw/meta-provider",
version: "2026.7.1-beta.3",
}),
},
{
name: "package/package.json",
contents: JSON.stringify({
name: "@openclaw/other",
version: "9.9.9",
}),
},
{
name: "package/openclaw.plugin.json",
contents: JSON.stringify({ id: "meta" }),
},
]);
await expect(
verifyClawHubPackedArtifactIdentity({
artifactPath: pack.artifactPath,
expectedSha256: pack.sha256,
expectedSize: String(pack.bytes.byteLength),
expectedDir: "extensions/meta",
expectedName: "@openclaw/meta-provider",
expectedVersion: "2026.7.1-beta.3",
}),
).rejects.toThrow("changes under the pinned ClawHub path normalization");
});
it("rejects a whitespace-bearing alias after a canonical package.json", async () => {
const pack = writeClawPack([
{
name: "package/package.json",
contents: JSON.stringify({
name: "@openclaw/meta-provider",
version: "2026.7.1-beta.3",
}),
},
{
name: " package.json ",
prefix: " package ",
contents: JSON.stringify({
name: "@openclaw/other",
version: "9.9.9",
}),
},
]);
await expect(
verifyClawHubPackedArtifactIdentity({
artifactPath: pack.artifactPath,
expectedSha256: pack.sha256,
expectedSize: String(pack.bytes.byteLength),
...expectedIdentity,
}),
).rejects.toThrow("changes under the pinned ClawHub path normalization");
});
it("rejects a compressed artifact above the ClawHub package limit before reading it", async () => {
const root = mkdtempSync(join(tmpdir(), "openclaw-clawhub-packed-limit-"));
tempDirs.push(root);
const artifactPath = join(root, "oversized.tgz");
writeFileSync(artifactPath, "");
truncateSync(artifactPath, 120 * 1024 * 1024 + 1);
await expect(
verifyClawHubPackedArtifactIdentity({
artifactPath,
expectedSha256: "a".repeat(64),
expectedSize: "1",
...expectedIdentity,
}),
).rejects.toThrow("exceeds 125829120 bytes");
});
it("bounds expanded tar bytes", async () => {
const root = mkdtempSync(join(tmpdir(), "openclaw-clawhub-expanded-limit-"));
tempDirs.push(root);
const artifactPath = join(root, "expanded.tgz");
const bytes = gzipSync(Buffer.alloc(64 * 1024 * 1024 + 1));
writeFileSync(artifactPath, bytes);
await expect(
verifyClawHubPackedArtifactIdentity({
artifactPath,
expectedSha256: createHash("sha256").update(bytes).digest("hex"),
expectedSize: String(bytes.byteLength),
...expectedIdentity,
}),
).rejects.toThrow("expands beyond 67108864 bytes");
});
it("bounds individual file payloads", async () => {
const pack = writeClawPack([
{
name: "package/large.bin",
contents: Buffer.alloc(50 * 1024 * 1024 + 1),
},
]);
await expect(
verifyClawHubPackedArtifactIdentity({
artifactPath: pack.artifactPath,
expectedSha256: pack.sha256,
expectedSize: String(pack.bytes.byteLength),
...expectedIdentity,
}),
).rejects.toThrow("entry package/large.bin exceeds 52428800 bytes");
});
it("bounds total file payload bytes", async () => {
const pack = writeClawPack(
Array.from({ length: 6 }, (_, index) => ({
name: `package/chunk-${index}.bin`,
contents: Buffer.alloc(9 * 1024 * 1024),
})),
);
await expect(
verifyClawHubPackedArtifactIdentity({
artifactPath: pack.artifactPath,
expectedSha256: pack.sha256,
expectedSize: String(pack.bytes.byteLength),
...expectedIdentity,
}),
).rejects.toThrow("file payload exceeds 52428800 bytes");
});
it("bounds the total number of TAR entries", async () => {
const entries: Array<{
name: string;
contents: string;
type: "0" | "5";
}> = Array.from({ length: 10_000 }, (_, index) => ({
name: `package/dir-${index}/`,
contents: "",
type: "5",
}));
entries.push({
name: "package/package.json",
contents: "{}",
type: "0",
});
const pack = writeClawPack(entries);
await expect(
verifyClawHubPackedArtifactIdentity({
artifactPath: pack.artifactPath,
expectedSha256: pack.sha256,
expectedSize: String(pack.bytes.byteLength),
...expectedIdentity,
}),
).rejects.toThrow("exceeds the 10000 entry limit");
});
});