Files
openclaw/scripts/lib/clawhub-bootstrap-artifact.mjs
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

804 lines
29 KiB
JavaScript

#!/usr/bin/env node
import { createHash } from "node:crypto";
import { lstat, mkdir, readdir, realpath, writeFile } from "node:fs/promises";
import { basename, dirname, join, relative, resolve, sep } from "node:path";
import { pathToFileURL } from "node:url";
import {
CLAWHUB_PUBLICATION_TAR_LIMITS,
inspectPackageTarballBytes,
validatePluginPackageManifest,
} from "../plugin-publication-artifact.mjs";
import {
describeActionsArtifactFiles,
readBoundedRegularFile,
readPublicationArtifactArchive,
} from "./actions-artifact-archive.mjs";
const SHA256_PATTERN = /^[a-f0-9]{64}$/u;
const SHA512_INTEGRITY_PATTERN = /^sha512-[A-Za-z0-9+/]{86}==$/u;
const COMMIT_PATTERN = /^[a-f0-9]{40}$/u;
const POSITIVE_INTEGER_PATTERN = /^[1-9][0-9]*$/u;
const REPOSITORY_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u;
const PACKAGE_NAME_PATTERN = /^@openclaw\/[a-z0-9][a-z0-9._-]*$/u;
const PACKAGE_DIR_PATTERN = /^extensions\/[a-z0-9][a-z0-9._-]*$/u;
const TAG_PATTERN = /^[a-z0-9][a-z0-9._-]*$/u;
const VERSION_PATTERN =
/^[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(?:-(?:alpha|beta)\.[1-9][0-9]*|-[1-9][0-9]*)?$/u;
const TOOLCHAIN_VERSION_PATTERN = /^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)$/u;
const MAX_BOOTSTRAP_ARCHIVE_BYTES = 256 * 1024 * 1024;
const MAX_BOOTSTRAP_ARCHIVE_FILES = 128;
const MAX_BOOTSTRAP_PACKAGES = MAX_BOOTSTRAP_ARCHIVE_FILES - 1;
const MAX_BOOTSTRAP_MANIFEST_BYTES = 2 * 1024 * 1024;
// The compressed and total-payload limits match ClawHub's ClawPack contract.
// The expanded TAR and entry-count ceilings bound this credential-job parser.
const MAX_CLAWPACK_BYTES = CLAWHUB_PUBLICATION_TAR_LIMITS.maxArchiveBytes;
function fail(message) {
throw new Error(message);
}
function compareCodeUnits(left, right) {
return left < right ? -1 : left > right ? 1 : 0;
}
function requireString(value, label) {
if (typeof value !== "string" || value.trim() === "") {
fail(`${label} is required.`);
}
return value.trim();
}
function requirePattern(value, pattern, label) {
const result = requireString(value, label);
if (!pattern.test(result)) {
fail(`${label} is invalid.`);
}
return result;
}
function requireBoolean(value, label) {
if (typeof value !== "boolean") {
fail(`${label} must be a boolean.`);
}
return value;
}
function parsePlugins(value) {
const plugins = requireString(value, "plugins")
.split(",")
.map((entry) => entry.trim())
.filter(Boolean);
const unique = [...new Set(plugins)].toSorted(compareCodeUnits);
if (unique.length !== plugins.length) {
fail("plugins must not contain duplicates.");
}
for (const plugin of unique) {
requirePattern(plugin, PACKAGE_NAME_PATTERN, `plugin ${plugin}`);
}
return unique;
}
function packageSlug(packageName) {
return packageName.slice("@openclaw/".length);
}
function normalizePlanEntry(value, index) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
fail(`matrix[${index}] must be an object.`);
}
const packageName = requirePattern(
value.packageName,
PACKAGE_NAME_PATTERN,
`matrix[${index}].packageName`,
);
const packageDir = requirePattern(
value.packageDir,
PACKAGE_DIR_PATTERN,
`matrix[${index}].packageDir`,
);
const publishTag = requirePattern(value.publishTag, TAG_PATTERN, `matrix[${index}].publishTag`);
const version = requirePattern(value.version, VERSION_PATTERN, `matrix[${index}].version`);
const bootstrapMode = requireString(value.bootstrapMode, `matrix[${index}].bootstrapMode`);
if (bootstrapMode !== "publish" && bootstrapMode !== "configure-only") {
fail(`matrix[${index}].bootstrapMode is invalid.`);
}
const requiresManualOverride = requireBoolean(
value.requiresManualOverride,
`matrix[${index}].requiresManualOverride`,
);
if (bootstrapMode === "configure-only" && !requiresManualOverride) {
fail(`matrix[${index}] configure-only entries must require the manual override.`);
}
return {
packageName,
version,
packageDir,
publishTag,
bootstrapMode,
requiresManualOverride,
};
}
function hashBytes(bytes) {
return {
sha256: createHash("sha256").update(bytes).digest("hex"),
size: bytes.byteLength,
};
}
async function hashFile(path) {
return hashBytes(
readBoundedRegularFile(path, {
label: "Packed ClawHub artifact",
maxBytes: MAX_CLAWPACK_BYTES,
}),
);
}
export async function verifyClawHubPackedArtifactIdentity(options) {
const artifactPath = resolve(requireString(options.artifactPath, "artifactPath"));
const expectedSha256 = requirePattern(options.expectedSha256, SHA256_PATTERN, "expectedSha256");
const expectedSize = requirePattern(
options.expectedSize,
POSITIVE_INTEGER_PATTERN,
"expectedSize",
);
const expectedName = requirePattern(options.expectedName, PACKAGE_NAME_PATTERN, "expectedName");
const expectedVersion = requireString(options.expectedVersion, "expectedVersion");
const expectedDir = requirePattern(options.expectedDir, PACKAGE_DIR_PATTERN, "expectedDir");
const artifactStat = await lstat(artifactPath);
if (!artifactStat.isFile() || artifactStat.isSymbolicLink()) {
fail("Packed ClawHub artifact must be a regular file.");
}
if (artifactStat.size > MAX_CLAWPACK_BYTES) {
fail(`Packed ClawHub artifact exceeds ${MAX_CLAWPACK_BYTES} bytes.`);
}
if (String(artifactStat.size) !== expectedSize) {
fail("Packed ClawHub artifact hash or size mismatch.");
}
const bytes = readBoundedRegularFile(artifactPath, {
label: "Packed ClawHub artifact",
maxBytes: MAX_CLAWPACK_BYTES,
});
const identity = hashBytes(bytes);
if (identity.sha256 !== expectedSha256 || String(identity.size) !== expectedSize) {
fail("Packed ClawHub artifact hash or size mismatch.");
}
const inspection = inspectPackageTarballBytes(bytes, CLAWHUB_PUBLICATION_TAR_LIMITS);
validatePluginPackageManifest(
{
packageDir: expectedDir,
packageName: expectedName,
route: "clawhub-token-bootstrap",
version: expectedVersion,
},
inspection.packageManifest,
);
const packageName = inspection.packageManifest.name;
const packageVersion = inspection.packageManifest.version;
return {
...identity,
inventory: inspection.inventory,
packageJsonSha256: inspection.packageManifestSha256,
packageName,
packageVersion,
pluginManifestSha256: inspection.pluginManifestSha256,
};
}
async function listFiles(root) {
const result = [];
let visitedEntries = 0;
let totalPathBytes = 0;
async function visit(directory, depth) {
if (depth > 4) {
fail("Artifact inventory exceeds the supported directory depth.");
}
for (const entry of await readdir(directory, { withFileTypes: true })) {
const path = join(directory, entry.name);
visitedEntries += 1;
if (visitedEntries > MAX_BOOTSTRAP_ARCHIVE_FILES * 4) {
fail("Artifact inventory contains too many filesystem entries.");
}
if (entry.isSymbolicLink()) {
fail(`Artifact inventory contains a symlink: ${relative(root, path)}`);
}
if (entry.isDirectory()) {
await visit(path, depth + 1);
} else if (entry.isFile()) {
const artifactPath = relative(root, path).split(sep).join("/");
totalPathBytes += Buffer.byteLength(artifactPath, "utf8");
if (
result.length >= MAX_BOOTSTRAP_ARCHIVE_FILES ||
totalPathBytes > MAX_BOOTSTRAP_MANIFEST_BYTES
) {
fail("Artifact inventory exceeds its file or path-byte limit.");
}
result.push(artifactPath);
} else {
fail(`Artifact inventory contains a non-regular entry: ${relative(root, path)}`);
}
}
}
await visit(root, 0);
return result.toSorted(compareCodeUnits);
}
function readPositiveInteger(value, label) {
const raw = requirePattern(value, POSITIVE_INTEGER_PATTERN, label);
const result = Number(raw);
if (!Number.isSafeInteger(result)) {
fail(`${label} is outside the supported range.`);
}
return result;
}
function requireExactKeys(value, expected, label) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
fail(`${label} must be an object.`);
}
const actual = Object.keys(value).toSorted(compareCodeUnits);
if (JSON.stringify(actual) !== JSON.stringify([...expected].toSorted(compareCodeUnits))) {
fail(`${label} keys are invalid: ${actual.join(",")}.`);
}
}
function normalizeBootstrapManifestEntry(value, index) {
requireExactKeys(
value,
[
"artifactPath",
"bootstrapMode",
"packageDir",
"packageName",
"publishTag",
"requiresManualOverride",
"sha256",
"size",
"version",
],
`manifest.entries[${index}]`,
);
const entry = normalizePlanEntry(value, index);
const expectedPrefix = `packages/${packageSlug(entry.packageName)}/`;
const artifactPath = requireString(value.artifactPath, `manifest.entries[${index}].artifactPath`);
if (
artifactPath !== `${expectedPrefix}${basename(artifactPath)}` ||
!/^[A-Za-z0-9][A-Za-z0-9._-]*\.tgz$/u.test(basename(artifactPath))
) {
fail(`Bootstrap Actions artifact path is invalid: ${artifactPath}`);
}
const sha256 = requirePattern(value.sha256, SHA256_PATTERN, `manifest.entries[${index}].sha256`);
if (!Number.isSafeInteger(value.size) || value.size <= 0 || value.size > MAX_CLAWPACK_BYTES) {
fail(`manifest.entries[${index}].size is invalid.`);
}
return { ...entry, artifactPath, sha256, size: value.size };
}
export function parseClawHubBootstrapManifestBytes(inputBytes) {
const manifestBytes = Buffer.isBuffer(inputBytes) ? inputBytes : Buffer.from(inputBytes);
if (manifestBytes.byteLength === 0 || manifestBytes.byteLength > MAX_BOOTSTRAP_MANIFEST_BYTES) {
fail(`ClawHub bootstrap manifest must be 1-${MAX_BOOTSTRAP_MANIFEST_BYTES} bytes.`);
}
let manifest;
try {
manifest = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(manifestBytes));
} catch {
fail("ClawHub bootstrap manifest is not valid UTF-8 JSON.");
}
requireExactKeys(
manifest,
[
"artifactName",
"clawhubToolchainIntegrity",
"clawhubToolchainSha256",
"clawhubToolchainVersion",
"entries",
"repository",
"requestedPlugins",
"runAttempt",
"runId",
"schemaVersion",
"targetSha",
"workflowSha",
],
"ClawHub bootstrap manifest",
);
if (manifest.schemaVersion !== 1) {
fail(`Unsupported ClawHub bootstrap manifest schema: ${String(manifest.schemaVersion)}.`);
}
const repository = requirePattern(manifest.repository, REPOSITORY_PATTERN, "manifest.repository");
const targetSha = requirePattern(manifest.targetSha, COMMIT_PATTERN, "manifest.targetSha");
const workflowSha = requirePattern(manifest.workflowSha, COMMIT_PATTERN, "manifest.workflowSha");
const runId = requirePattern(manifest.runId, POSITIVE_INTEGER_PATTERN, "manifest.runId");
const runAttempt = requirePattern(
manifest.runAttempt,
POSITIVE_INTEGER_PATTERN,
"manifest.runAttempt",
);
const artifactName = requireString(manifest.artifactName, "manifest.artifactName");
const clawhubToolchainSha256 = requirePattern(
manifest.clawhubToolchainSha256,
SHA256_PATTERN,
"manifest.clawhubToolchainSha256",
);
const clawhubToolchainVersion = requirePattern(
manifest.clawhubToolchainVersion,
TOOLCHAIN_VERSION_PATTERN,
"manifest.clawhubToolchainVersion",
);
const clawhubToolchainIntegrity = requirePattern(
manifest.clawhubToolchainIntegrity,
SHA512_INTEGRITY_PATTERN,
"manifest.clawhubToolchainIntegrity",
);
if (
!Array.isArray(manifest.requestedPlugins) ||
manifest.requestedPlugins.length === 0 ||
manifest.requestedPlugins.length > MAX_BOOTSTRAP_PACKAGES ||
manifest.requestedPlugins.some((entry) => typeof entry !== "string")
) {
fail("ClawHub bootstrap manifest requestedPlugins is invalid.");
}
const requestedPlugins = parsePlugins(manifest.requestedPlugins.join(","));
if (JSON.stringify(requestedPlugins) !== JSON.stringify(manifest.requestedPlugins)) {
fail("ClawHub bootstrap manifest requestedPlugins is not canonical.");
}
if (
!Array.isArray(manifest.entries) ||
manifest.entries.length === 0 ||
manifest.entries.length > MAX_BOOTSTRAP_PACKAGES
) {
fail("ClawHub bootstrap manifest entries are invalid.");
}
const entries = manifest.entries.map(normalizeBootstrapManifestEntry);
if (new Set(entries.map((entry) => entry.packageName)).size !== entries.length) {
fail("ClawHub bootstrap manifest contains duplicate package names.");
}
assertExactPackageSet(entries, requestedPlugins);
const entryNames = entries.map((entry) => entry.packageName);
if (JSON.stringify(entryNames) !== JSON.stringify(entryNames.toSorted(compareCodeUnits))) {
fail("ClawHub bootstrap manifest entries are not canonical.");
}
return {
artifactName,
clawhubToolchainIntegrity,
clawhubToolchainSha256,
clawhubToolchainVersion,
entries,
repository,
requestedPlugins,
runAttempt,
runId,
schemaVersion: 1,
targetSha,
workflowSha,
};
}
export function readClawHubBootstrapManifest(path) {
return parseClawHubBootstrapManifestBytes(
readBoundedRegularFile(path, {
label: "ClawHub bootstrap manifest",
maxBytes: MAX_BOOTSTRAP_MANIFEST_BYTES,
}),
);
}
function validateBootstrapArchiveInventory(files, expectedBinding) {
const manifestBytes = files.get("manifest.json");
if (!manifestBytes) {
fail("Bootstrap Actions artifact must contain manifest.json.");
}
const manifest = parseClawHubBootstrapManifestBytes(manifestBytes);
for (const [key, expected] of Object.entries(expectedBinding)) {
if (manifest[key] !== String(expected)) {
fail(`Bootstrap Actions artifact manifest ${key} mismatch.`);
}
}
const { entries, requestedPlugins } = manifest;
const expected = new Set(["manifest.json"]);
for (const entry of entries) {
const artifactPath = entry.artifactPath;
if (expected.has(artifactPath)) {
fail(`Bootstrap Actions artifact path is duplicated: ${artifactPath}`);
}
const bytes = files.get(artifactPath);
if (!bytes || bytes.byteLength !== entry.size || hashBytes(bytes).sha256 !== entry.sha256) {
fail(`Bootstrap Actions artifact bytes do not match manifest: ${artifactPath}`);
}
expected.add(artifactPath);
}
const actual = new Set(files.keys());
if (actual.size !== expected.size || [...actual].some((path) => !expected.has(path))) {
fail(
`Bootstrap Actions artifact inventory mismatch: expected ${[...expected].toSorted(compareCodeUnits).join(",")}, found ${[...actual].toSorted(compareCodeUnits).join(",")}.`,
);
}
return { entries, manifest, requestedPlugins };
}
export async function downloadClawHubBootstrapArtifact(options) {
const artifactId = readPositiveInteger(options.artifactId, "artifactId");
const artifactSizeBytes = readPositiveInteger(options.artifactSize, "artifactSize");
const runId = readPositiveInteger(options.runId, "runId");
const runAttempt = readPositiveInteger(options.runAttempt, "runAttempt");
const consumerRunAttempt = readPositiveInteger(options.consumerRunAttempt, "consumerRunAttempt");
const producerJobName = requireString(options.producerJobName, "producerJobName");
const targetSha = requirePattern(options.targetSha, COMMIT_PATTERN, "targetSha");
const workflowSha = requirePattern(options.workflowSha, COMMIT_PATTERN, "workflowSha");
const artifactDigest = requirePattern(options.artifactDigest, SHA256_PATTERN, "artifactDigest");
const artifactName = requireString(options.artifactName, "artifactName");
const repository = requirePattern(options.repository, REPOSITORY_PATTERN, "repository");
const clawhubToolchainSha256 = requirePattern(
options.clawhubToolchainSha256,
SHA256_PATTERN,
"clawhubToolchainSha256",
);
const clawhubToolchainVersion = requirePattern(
options.clawhubToolchainVersion,
TOOLCHAIN_VERSION_PATTERN,
"clawhubToolchainVersion",
);
const clawhubToolchainIntegrity = requirePattern(
options.clawhubToolchainIntegrity,
SHA512_INTEGRITY_PATTERN,
"clawhubToolchainIntegrity",
);
const expectedName = `clawhub-bootstrap-${targetSha.slice(0, 12)}-${runId}-${runAttempt}`;
if (artifactName !== expectedName) {
fail("ClawHub bootstrap artifact name does not bind the target and producer attempt.");
}
const outputRoot = resolve(requireString(options.outputRoot, "outputRoot"));
try {
await lstat(outputRoot);
fail("ClawHub bootstrap artifact output directory must not already exist.");
} catch (error) {
if (!error || typeof error !== "object" || error.code !== "ENOENT") {
throw error;
}
}
const result = await readPublicationArtifactArchive({
archivePolicy: {
minEntries: 2,
maxEntries: MAX_BOOTSTRAP_ARCHIVE_FILES,
maxArchiveBytes: MAX_BOOTSTRAP_ARCHIVE_BYTES,
maxExpandedBytes: MAX_BOOTSTRAP_ARCHIVE_BYTES,
allowPath: (path) =>
path === "manifest.json" ||
/^packages\/[a-z0-9][a-z0-9._-]*\/[A-Za-z0-9][A-Za-z0-9._-]*\.tgz$/u.test(path),
maxCompressedEntryBytes: (path) =>
path === "manifest.json" ? MAX_BOOTSTRAP_MANIFEST_BYTES : MAX_CLAWPACK_BYTES,
maxEntryBytes: (path) =>
path === "manifest.json" ? MAX_BOOTSTRAP_MANIFEST_BYTES : MAX_CLAWPACK_BYTES,
},
expected: {
artifactDigest: `sha256:${artifactDigest}`,
artifactId,
artifactName,
artifactSizeBytes,
consumerRunAttempt,
producerJobName,
repository,
runStatePolicy: "same-run-producer-success",
runAttempt,
runId,
workflowEvent: "workflow_dispatch",
workflowHeadBranch: "main",
workflowPath: ".github/workflows/plugin-clawhub-new.yml",
workflowSha,
},
maxArchiveBytes: MAX_BOOTSTRAP_ARCHIVE_BYTES,
fetchImpl: options.fetchImpl,
retryAttempts: options.retryAttempts,
retryDelayMs: options.retryDelayMs,
token: requireString(options.token, "token"),
});
const validated = validateBootstrapArchiveInventory(result.files, {
artifactName,
clawhubToolchainIntegrity,
clawhubToolchainSha256,
clawhubToolchainVersion,
repository,
runAttempt,
runId,
targetSha,
workflowSha,
});
await mkdir(dirname(outputRoot), { mode: 0o700, recursive: true });
try {
await mkdir(outputRoot, { mode: 0o700 });
} catch (error) {
if (error && typeof error === "object" && error.code === "EEXIST") {
fail("ClawHub bootstrap artifact output directory must not already exist.");
}
throw error;
}
const outputRootStat = await lstat(outputRoot);
if (!outputRootStat.isDirectory() || outputRootStat.isSymbolicLink()) {
fail("ClawHub bootstrap artifact output directory must be newly created.");
}
for (const [path, bytes] of result.files) {
const destination = join(outputRoot, path);
await mkdir(dirname(destination), { mode: 0o700, recursive: true });
await writeFile(destination, bytes, { flag: "wx", mode: 0o600 });
}
return {
artifactDigest,
artifactId,
artifactName,
artifactSizeBytes,
clawhubToolchainIntegrity,
clawhubToolchainSha256,
clawhubToolchainVersion,
inventory: describeActionsArtifactFiles(result.files),
packages: validated.entries,
runAttempt,
runId,
};
}
async function resolveRegularArtifactFile(root, artifactPath) {
if (
typeof artifactPath !== "string" ||
artifactPath.startsWith("/") ||
artifactPath.includes("\\") ||
artifactPath.split("/").some((part) => part === "" || part === "." || part === "..")
) {
fail(`Unsafe artifact path: ${String(artifactPath)}`);
}
const rootReal = await realpath(root);
const candidate = resolve(root, artifactPath);
const candidateReal = await realpath(candidate);
if (candidateReal !== rootReal && !candidateReal.startsWith(`${rootReal}${sep}`)) {
fail(`Artifact path escapes the artifact root: ${artifactPath}`);
}
const fileStat = await lstat(candidate);
if (!fileStat.isFile() || fileStat.isSymbolicLink()) {
fail(`Artifact path is not a regular file: ${artifactPath}`);
}
return candidate;
}
function assertExactPackageSet(entries, expectedPlugins) {
const actual = entries.map((entry) => entry.packageName).toSorted(compareCodeUnits);
if (JSON.stringify(actual) !== JSON.stringify(expectedPlugins)) {
fail(
`Artifact package set does not match requested plugins: expected ${expectedPlugins.join(",")}, found ${actual.join(",")}.`,
);
}
}
export async function createClawHubBootstrapArtifactManifest(options) {
const artifactRoot = resolve(options.artifactRoot);
const matrix = JSON.parse(
readBoundedRegularFile(options.matrixPath, {
label: "ClawHub bootstrap matrix",
maxBytes: MAX_BOOTSTRAP_MANIFEST_BYTES,
}).toString("utf8"),
);
if (!Array.isArray(matrix) || matrix.length === 0 || matrix.length > MAX_BOOTSTRAP_PACKAGES) {
fail("matrix must be a non-empty array.");
}
const entries = matrix.map(normalizePlanEntry);
const expectedPlugins = parsePlugins(options.plugins);
if (new Set(entries.map((entry) => entry.packageName)).size !== entries.length) {
fail("matrix must not contain duplicate package names.");
}
assertExactPackageSet(entries, expectedPlugins);
const manifestEntries = [];
for (const entry of entries.toSorted((a, b) => compareCodeUnits(a.packageName, b.packageName))) {
const packageDirectory = join(artifactRoot, "packages", packageSlug(entry.packageName));
const files = (await readdir(packageDirectory)).filter((name) => name.endsWith(".tgz"));
if (files.length !== 1) {
fail(`${entry.packageName} must have exactly one packed .tgz artifact.`);
}
const artifactPath = `packages/${packageSlug(entry.packageName)}/${files[0]}`;
const filePath = await resolveRegularArtifactFile(artifactRoot, artifactPath);
const identity = await hashFile(filePath);
manifestEntries.push({ ...entry, artifactPath, ...identity });
}
const manifest = {
schemaVersion: 1,
repository: requirePattern(options.repository, REPOSITORY_PATTERN, "repository"),
targetSha: requirePattern(options.targetSha, COMMIT_PATTERN, "targetSha"),
workflowSha: requirePattern(options.workflowSha, COMMIT_PATTERN, "workflowSha"),
runId: requirePattern(options.runId, POSITIVE_INTEGER_PATTERN, "runId"),
runAttempt: requirePattern(options.runAttempt, POSITIVE_INTEGER_PATTERN, "runAttempt"),
artifactName: requireString(options.artifactName, "artifactName"),
clawhubToolchainIntegrity: requirePattern(
options.clawhubToolchainIntegrity,
SHA512_INTEGRITY_PATTERN,
"clawhubToolchainIntegrity",
),
clawhubToolchainSha256: requirePattern(
options.clawhubToolchainSha256,
SHA256_PATTERN,
"clawhubToolchainSha256",
),
clawhubToolchainVersion: requirePattern(
options.clawhubToolchainVersion,
TOOLCHAIN_VERSION_PATTERN,
"clawhubToolchainVersion",
),
requestedPlugins: expectedPlugins,
entries: manifestEntries,
};
await mkdir(dirname(options.outputPath), { recursive: true });
await writeFile(options.outputPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
return manifest;
}
export async function verifyClawHubBootstrapArtifactManifest(options) {
const artifactRoot = resolve(options.artifactRoot);
const manifest = readClawHubBootstrapManifest(options.manifestPath);
const expected = {
repository: requirePattern(options.repository, REPOSITORY_PATTERN, "repository"),
targetSha: requirePattern(options.targetSha, COMMIT_PATTERN, "targetSha"),
workflowSha: requirePattern(options.workflowSha, COMMIT_PATTERN, "workflowSha"),
runId: requirePattern(options.runId, POSITIVE_INTEGER_PATTERN, "runId"),
runAttempt: requirePattern(options.runAttempt, POSITIVE_INTEGER_PATTERN, "runAttempt"),
artifactName: requireString(options.artifactName, "artifactName"),
clawhubToolchainIntegrity: requirePattern(
options.clawhubToolchainIntegrity,
SHA512_INTEGRITY_PATTERN,
"clawhubToolchainIntegrity",
),
clawhubToolchainSha256: requirePattern(
options.clawhubToolchainSha256,
SHA256_PATTERN,
"clawhubToolchainSha256",
),
clawhubToolchainVersion: requirePattern(
options.clawhubToolchainVersion,
TOOLCHAIN_VERSION_PATTERN,
"clawhubToolchainVersion",
),
};
for (const [key, value] of Object.entries(expected)) {
if (manifest[key] !== value) {
fail(`Bootstrap artifact manifest ${key} mismatch.`);
}
}
const expectedPlugins = parsePlugins(options.plugins);
if (!Array.isArray(manifest.requestedPlugins)) {
fail("Bootstrap artifact manifest requestedPlugins must be an array.");
}
if (JSON.stringify(manifest.requestedPlugins) !== JSON.stringify(expectedPlugins)) {
fail("Bootstrap artifact manifest requestedPlugins mismatch.");
}
if (
!Array.isArray(manifest.entries) ||
manifest.entries.length === 0 ||
manifest.entries.length > MAX_BOOTSTRAP_PACKAGES
) {
fail("Bootstrap artifact manifest entries must be a non-empty array.");
}
const entries = [];
const allowedFiles = new Set([relative(artifactRoot, options.manifestPath).split(sep).join("/")]);
for (const [index, rawEntry] of manifest.entries.entries()) {
const entry = normalizeBootstrapManifestEntry(rawEntry, index);
const { artifactPath } = entry;
const filePath = await resolveRegularArtifactFile(artifactRoot, artifactPath);
const identity = await hashFile(filePath);
if (identity.sha256 !== entry.sha256 || identity.size !== entry.size) {
fail(`${entry.packageName} packed artifact hash or size mismatch.`);
}
allowedFiles.add(artifactPath);
entries.push({ ...entry, artifactPath, ...identity });
}
if (new Set(entries.map((entry) => entry.packageName)).size !== entries.length) {
fail("Bootstrap artifact manifest must not contain duplicate package names.");
}
assertExactPackageSet(entries, expectedPlugins);
const inventory = await listFiles(artifactRoot);
const expectedInventory = [...allowedFiles].toSorted(compareCodeUnits);
if (JSON.stringify(inventory) !== JSON.stringify(expectedInventory)) {
fail(
`Bootstrap artifact inventory mismatch: expected ${expectedInventory.join(",")}, found ${inventory.join(",")}.`,
);
}
return { ...manifest, entries };
}
function parseArgs(argv) {
const values = [...argv];
const command = values.shift();
const result = { command };
while (values.length > 0) {
const key = values.shift();
const value = values.shift();
if (!key?.startsWith("--") || value === undefined) {
fail(`Invalid argument: ${String(key)}`);
}
result[key.slice(2).replaceAll("-", "_")] = value;
}
return result;
}
async function main() {
const args = parseArgs(process.argv.slice(2));
if (args.command === "download") {
const result = await downloadClawHubBootstrapArtifact({
artifactDigest: args.artifact_digest,
artifactId: args.artifact_id,
artifactName: args.artifact_name,
artifactSize: args.artifact_size,
consumerRunAttempt: args.consumer_run_attempt,
clawhubToolchainIntegrity: args.clawhub_toolchain_integrity,
clawhubToolchainSha256: args.clawhub_toolchain_sha256,
clawhubToolchainVersion: args.clawhub_toolchain_version,
outputRoot: args.output_root,
producerJobName: args.producer_job_name,
repository: args.repository,
runAttempt: args.run_attempt,
runId: args.run_id,
targetSha: args.target_sha,
token: process.env.GH_TOKEN,
workflowSha: args.workflow_sha,
});
process.stdout.write(`${JSON.stringify(result)}\n`);
return;
}
if (args.command === "verify-packed") {
const identity = await verifyClawHubPackedArtifactIdentity({
artifactPath: args.path,
expectedDir: args.expected_dir,
expectedSha256: args.expected_sha256,
expectedSize: args.expected_size,
expectedName: args.expected_name,
expectedVersion: args.expected_version,
});
process.stdout.write(`${JSON.stringify(identity)}\n`);
return;
}
const common = {
artifactRoot: args.artifact_root,
artifactName: args.artifact_name,
clawhubToolchainIntegrity: args.clawhub_toolchain_integrity,
clawhubToolchainSha256: args.clawhub_toolchain_sha256,
clawhubToolchainVersion: args.clawhub_toolchain_version,
repository: args.repository,
targetSha: args.target_sha,
workflowSha: args.workflow_sha,
runId: args.run_id,
runAttempt: args.run_attempt,
plugins: args.plugins,
};
if (args.command === "create") {
await createClawHubBootstrapArtifactManifest({
...common,
matrixPath: args.matrix,
outputPath: args.output,
});
return;
}
if (args.command === "verify") {
const manifest = await verifyClawHubBootstrapArtifactManifest({
...common,
manifestPath: args.manifest,
});
if (args.output) {
await mkdir(dirname(args.output), { recursive: true });
await writeFile(args.output, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
} else {
process.stdout.write(`${JSON.stringify(manifest, null, 2)}\n`);
}
return;
}
fail("Usage: clawhub-bootstrap-artifact.mjs <create|download|verify|verify-packed> [options]");
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
await main();
}