Files
clawhub/scripts/claws-feed-openclaw-e2e.test.ts
T
Gio Della-LiberaandPatrick Erichsen 5a3b050751 Add gated Claw hosted feed and lifecycle proof (#3092)
* feat(claws): publish hosted feed with OpenClaw proof

* test(claws): prove package-local profile feed flow

* fix(claws): encode scoped package artifact routes

* fix(claws): enforce feed rollback and binding

* test(claws): pin hosted OpenClaw contract proof

* test(claws): add Convex feed runtime smoke

* chore(schema): refresh experimental feed declarations

---------

Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
2026-07-24 19:29:12 -05:00

276 lines
9.7 KiB
TypeScript

import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { createServer, type Server } from "node:http";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { promisify } from "node:util";
import { EXPERIMENTAL_CLAW_FEED_ID, serializeExperimentalClawFeed } from "clawhub-schema";
import { gzipSync, strToU8, zipSync } from "fflate";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import {
assertSafeClawArchive,
extractSafeClawZip,
findExtractedPackageRoot,
readResponseBytesBounded,
runPublishedClawDryRun,
selectPublishedClaw,
} from "./claws-feed-openclaw-e2e";
const execFileAsync = promisify(execFile);
const openclawRepo = process.env.OPENCLAW_CLAWS_CHECKOUT;
const fixtureRoot = resolve("fixtures/claws/hosted-e2e");
let tempRoot = "";
let archiveBytes = new Uint8Array();
let integrity = "";
let server: Server | undefined;
let serverPort = 0;
const TAR_BLOCK_SIZE = 512;
function writeTarString(target: Uint8Array, offset: number, width: number, value: string) {
target.set(new TextEncoder().encode(value).subarray(0, width), offset);
}
function writeTarOctal(target: Uint8Array, offset: number, width: number, value: number) {
writeTarString(target, offset, width, `${value.toString(8).padStart(width - 1, "0")}\0`);
}
function tarEntry(path: string, type: "0" | "2", content = new Uint8Array()) {
const header = new Uint8Array(TAR_BLOCK_SIZE);
writeTarString(header, 0, 100, path);
writeTarOctal(header, 100, 8, type === "0" ? 0o644 : 0o777);
writeTarOctal(header, 108, 8, 0);
writeTarOctal(header, 116, 8, 0);
writeTarOctal(header, 124, 12, content.byteLength);
writeTarOctal(header, 136, 12, 0);
header.fill(0x20, 148, 156);
header[156] = type.charCodeAt(0);
if (type === "2") writeTarString(header, 157, 100, "../../outside");
writeTarString(header, 257, 6, "ustar");
writeTarString(header, 263, 2, "00");
writeTarOctal(
header,
148,
8,
header.reduce((sum, byte) => sum + byte, 0),
);
const body = new Uint8Array(Math.ceil(content.byteLength / TAR_BLOCK_SIZE) * TAR_BLOCK_SIZE);
body.set(content);
return [header, body];
}
function deterministicLinkArchive() {
const parts = [
...tarEntry(
"package/package.json",
"0",
new TextEncoder().encode('{"name":"@openclaw/hosted-e2e","version":"1.0.0"}\n'),
),
...tarEntry("package/workspace", "2"),
new Uint8Array(TAR_BLOCK_SIZE * 2),
];
const tar = new Uint8Array(parts.reduce((size, part) => size + part.byteLength, 0));
let offset = 0;
for (const part of parts) {
tar.set(part, offset);
offset += part.byteLength;
}
return gzipSync(tar);
}
async function npmPackFixture(destination: string) {
const { stdout } = await execFileAsync(
"npm",
[
"pack",
join(fixtureRoot, "package"),
"--json",
"--ignore-scripts",
"--pack-destination",
destination,
],
{ cwd: destination },
);
const output = JSON.parse(stdout) as unknown;
const filename =
Array.isArray(output) && typeof output[0]?.filename === "string"
? output[0].filename
: undefined;
if (!filename) throw new Error("npm pack did not return a fixture filename");
return join(destination, filename);
}
function feedValue() {
const now = Date.now();
return JSON.parse(
serializeExperimentalClawFeed({
schemaVersion: 1,
id: EXPERIMENTAL_CLAW_FEED_ID,
generatedAt: new Date(now).toISOString(),
sequence: 1,
expiresAt: new Date(now + 86_400_000).toISOString(),
entries: [
{
type: "claw",
id: "@openclaw/hosted-e2e",
title: "Hosted E2E",
version: "1.0.0",
state: "available",
publisher: { id: "openclaw", trust: "official" },
clawManifestSummary: {
schemaVersion: 1,
agent: { id: "hosted-e2e", name: "Hosted E2E" },
workspace: { bootstrapFiles: ["SOUL.md"], fileCount: 0 },
packages: { skillCount: 0, pluginCount: 0 },
mcpServerCount: 0,
cronJobCount: 0,
},
install: {
candidates: [
{
sourceRef: "public-clawhub",
package: "@openclaw/hosted-e2e",
version: "1.0.0",
integrity,
},
],
},
},
],
}),
);
}
describe("published Claw to OpenClaw dry-run proof", () => {
beforeAll(async () => {
tempRoot = await mkdtemp(join(tmpdir(), "clawhub-hosted-e2e-fixture-"));
const archivePath = await npmPackFixture(tempRoot);
archiveBytes = new Uint8Array(await readFile(archivePath));
integrity = `sha256:${createHash("sha256").update(archiveBytes).digest("hex")}`;
server = createServer((request, response) => {
const pathname = new URL(request.url ?? "/", "http://localhost").pathname;
if (pathname === "/v1/feeds/claws") {
response.setHeader("Content-Type", "application/json");
response.end(JSON.stringify(feedValue()));
return;
}
if (pathname === "/api/v1/packages/%40openclaw%2Fhosted-e2e/versions/1.0.0/artifact") {
response.setHeader("Content-Type", "application/json");
response.end(
JSON.stringify({
artifact: {
kind: "npm-pack",
sha256: integrity.slice("sha256:".length),
downloadUrl: "/download.tgz",
},
}),
);
return;
}
if (pathname === "/download.tgz") {
response.setHeader("Content-Type", "application/gzip");
response.end(archiveBytes);
return;
}
response.statusCode = 404;
response.end("Not found");
});
await new Promise<void>((resolveListen) => server!.listen(0, "127.0.0.1", resolveListen));
const address = server.address();
if (!address || typeof address === "string") throw new Error("Fixture server did not bind");
serverPort = address.port;
});
afterAll(async () => {
if (server) await new Promise<void>((resolveClose) => server!.close(() => resolveClose()));
if (tempRoot) await rm(tempRoot, { recursive: true, force: true });
});
it("selects only the exact public ClawHub candidate", () => {
const selected = selectPublishedClaw(feedValue(), "@openclaw/hosted-e2e");
expect(selected.candidate).toMatchObject({ version: "1.0.0", integrity });
expect(() => selectPublishedClaw(feedValue(), "@openclaw/missing")).toThrow("was not present");
});
it("builds a portable production-equivalent ClawPack fixture", async () => {
const archivePath = join(tempRoot, "portable-claw.tgz");
await writeFile(archivePath, archiveBytes);
await expect(assertSafeClawArchive(archivePath)).resolves.toBeUndefined();
});
it("rejects link entries before extracting a published artifact", async () => {
const archivePath = join(tempRoot, "linked.tgz");
await writeFile(archivePath, deterministicLinkArchive());
await expect(assertSafeClawArchive(archivePath)).rejects.toThrow(
"only contain regular files and directories",
);
});
it("extracts legacy ZIP artifacts without permitting traversal", async () => {
const root = await mkdtemp(join(tmpdir(), "clawhub-hosted-e2e-zip-"));
try {
const archive = zipSync({
"package/package.json": strToU8("{}\n"),
"package/CLAW.md": strToU8("---\nschemaVersion: 1\n---\n"),
});
await extractSafeClawZip(archive, root);
await expect(readFile(join(root, "package", "package.json"), "utf8")).resolves.toBe("{}\n");
const unsafeArchive = zipSync({ "../outside": strToU8("unsafe") });
await expect(extractSafeClawZip(unsafeArchive, root)).rejects.toThrow("unsafe path");
} finally {
await rm(root, { recursive: true, force: true });
}
});
it("discovers legacy ZIP packages extracted directly at the archive root", async () => {
const root = await mkdtemp(join(tmpdir(), "clawhub-hosted-e2e-root-zip-"));
try {
const archive = zipSync({
"package.json": strToU8("{}\n"),
"CLAW.md": strToU8("---\nschemaVersion: 1\n---\n"),
});
await extractSafeClawZip(archive, root);
await expect(findExtractedPackageRoot(root)).resolves.toBe(root);
} finally {
await rm(root, { recursive: true, force: true });
}
});
it("rejects ZIP artifacts whose expanded content exceeds the package limit", async () => {
const root = await mkdtemp(join(tmpdir(), "clawhub-hosted-e2e-large-zip-"));
try {
const archive = zipSync({ "package/large.bin": new Uint8Array(50 * 1024 * 1024 + 1) });
await expect(extractSafeClawZip(archive, root)).rejects.toThrow("50MB unpacked limit");
} finally {
await rm(root, { recursive: true, force: true });
}
});
it("rejects oversized downloads from metadata before buffering the body", async () => {
const response = new Response("", { headers: { "Content-Length": String(65 * 1024 * 1024) } });
await expect(readResponseBytesBounded(response)).rejects.toThrow("64MB download limit");
});
it.skipIf(!openclawRepo)(
"runs the downloaded package through OpenClaw dry-run",
async () => {
const origin = `http://127.0.0.1:${serverPort}`;
const result = await runPublishedClawDryRun({
feedUrl: `${origin}/v1/feeds/claws`,
packageName: "@openclaw/hosted-e2e",
registryUrl: origin,
openclawRepo: openclawRepo!,
});
expect(result.plan).toMatchObject({
schemaVersion: "openclaw.clawAddPlan.v1",
dryRun: true,
mutationAllowed: false,
agent: { finalId: "hosted-e2e" },
summary: { blockedActions: 0 },
});
},
30_000,
);
});