feat: componentize GStack 2 runtime and release integrity

This commit is contained in:
Sinabina
2026-07-20 14:16:23 -07:00
parent b0ea2296d1
commit f14445bb00
270 changed files with 9681 additions and 51572 deletions
@@ -0,0 +1,87 @@
#!/usr/bin/env node
import fs from "node:fs/promises";
import path from "node:path";
const [directory, repository = process.env.GITHUB_REPOSITORY, version = "2.0.0"] = process.argv.slice(2);
if (!directory || !repository) {
console.error("Usage: create-runtime-release-manifest.mjs <artifact-dir> <owner/repo> [version]");
process.exit(2);
}
const targets = [
"darwin-arm64",
"darwin-x64",
"linux-arm64",
"linux-x64",
"windows-arm64",
"windows-x64",
];
const componentDependencies = {
core: [],
"browser-code": ["core"],
"browser-headless": ["browser-code"],
"browser-visible": ["browser-code"],
design: ["core"],
diagram: ["browser-headless"],
pdf: ["diagram"],
ios: ["core"],
};
const capabilityComponents = {
browser: ["browser-code", "browser-headless"],
"browser-visible": ["browser-code", "browser-visible"],
design: ["design"],
diagram: ["diagram"],
pdf: ["pdf"],
ios: ["ios"],
};
const commonComponents = ["core", "browser-code", "browser-headless", "browser-visible", "design", "diagram", "pdf"];
const release = `https://github.com/${repository}/releases/download/v${version}`;
const certificateIdentity = `https://github.com/${repository}/.github/workflows/release-artifacts.yml@refs/tags/v${version}`;
const targetRecords = {};
for (const target of targets) {
const ids = [...commonComponents, ...(target.startsWith("darwin-") ? ["ios"] : [])];
const components = {};
for (const component of ids) {
const name = `gstack-runtime-${version}-${target}-${component}.tar.gz`;
const archive = path.join(directory, name);
const bundle = `${archive}.sigstore.json`;
const digestFile = `${archive}.sha256`;
const [stat, digest, bundleStat] = await Promise.all([
fs.stat(archive),
fs.readFile(digestFile, "utf8"),
fs.stat(bundle),
]);
const sha256 = digest.trim().split(/\s+/)[0];
if (!stat.isFile() || stat.size <= 0 || stat.size > 2 * 1024 * 1024 * 1024) {
throw new Error(`Invalid artifact size for ${name}: ${stat.size}`);
}
if (!bundleStat.isFile() || bundleStat.size <= 0) throw new Error(`Missing Sigstore bundle for ${name}`);
if (!/^[a-f0-9]{64}$/.test(sha256)) throw new Error(`Invalid SHA-256 for ${name}`);
components[component] = {
url: `${release}/${name}`,
sha256,
bytes: stat.size,
format: "tar.gz",
root: "gstack",
cosignBundleUrl: `${release}/${name}.sigstore.json`,
certificateIdentity,
certificateOidcIssuer: "https://token.actions.githubusercontent.com",
};
}
targetRecords[target] = { components };
}
const manifest = {
schemaVersion: 2,
version,
skillApi: "2.0",
capabilityComponents,
componentDependencies,
targets: targetRecords,
};
await fs.writeFile(
path.join(directory, "gstack-runtime-manifest.json"),
`${JSON.stringify(manifest, null, 2)}\n`,
{ flag: "wx", mode: 0o644 },
);
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env node
import { spawn } from "node:child_process";
const child = spawn("bun", [
"bin/gstack-redact",
"--repo-visibility", "public",
"--json",
"--max-bytes", "16000000",
], { shell: false, windowsHide: true, stdio: ["pipe", "pipe", "inherit"] });
let diff = "";
process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk) => { diff += chunk; });
process.stdin.once("end", () => {
const additions = diff
.split(/\r?\n/)
.filter((line) => line.startsWith("+") && !line.startsWith("+++"))
.map((line) => line.slice(1))
.join("\n");
child.stdin.end(additions);
});
let stdout = "";
child.stdout.setEncoding("utf8");
child.stdout.on("data", (chunk) => { stdout += chunk; });
child.once("error", (error) => { throw error; });
child.once("close", (code) => {
const report = JSON.parse(stdout);
const high = Number(report.counts?.HIGH ?? 0);
const medium = Number(report.counts?.MEDIUM ?? 0);
console.log(`credential scan: ${high} high, ${medium} advisory`);
process.exitCode = high > 0 || report.oversize || ![0, 2, 3].includes(code) ? 1 : 0;
});
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env node
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { spawn } from "node:child_process";
const root = path.resolve(process.argv[2] ?? process.cwd());
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "gstack-npm-smoke-"));
try {
const pack = await run("npm", ["pack", "--json", "--ignore-scripts", "--pack-destination", temporary], root);
const [metadata] = JSON.parse(pack.stdout);
if (!metadata?.filename || metadata.size > 2_000_000 || metadata.entryCount > 80) {
throw new Error(`Unexpected npm package shape: ${JSON.stringify(metadata)}`);
}
const archive = path.join(temporary, metadata.filename);
const project = path.join(temporary, "consumer");
await fs.mkdir(project);
await fs.writeFile(path.join(project, "package.json"), '{"private":true}\n');
await run("npm", ["install", "--ignore-scripts", "--no-audit", "--no-fund", "--package-lock=false", archive], project);
const installed = path.join(project, "node_modules", "gstack");
const pkg = JSON.parse(await fs.readFile(path.join(installed, "package.json"), "utf8"));
if (pkg.gstack?.packageRole !== "runtime-control") throw new Error("Packed role metadata is missing");
for (const excluded of ["skills", "browse", "design", "make-pdf", "setup"]) {
if (await exists(path.join(installed, excluded))) throw new Error(`Packed package unexpectedly contains ${excluded}`);
}
const state = path.join(temporary, "state");
const env = { ...process.env, GSTACK_HOME: state };
const version = await run(process.execPath, [path.join(installed, "bin", "gstack"), "--version"], project, env);
if (!version.stdout.includes(`gstack runtime ${pkg.gstack.runtimeVersion}`)) throw new Error("Packed gstack version mismatch");
await run(process.execPath, [path.join(installed, "bin", "gstack"), "setup"], project, env);
const doctor = await run(process.execPath, [path.join(installed, "bin", "gstack"), "doctor", "--json"], project, env, [0, 1]);
const report = JSON.parse(doctor.stdout);
if (!Array.isArray(report.checks) || !report.checks.some((check) => check.id === "config" && check.status === "pass")) {
throw new Error("Packed setup/doctor did not initialize isolated runtime state");
}
const help = await run(process.execPath, [path.join(installed, "runtime", "runtime-bootstrap.mjs"), "--help"], project, env);
if (!help.stdout.includes("Usage:")) {
throw new Error(`Packed runtime bootstrap help is unavailable: ${JSON.stringify(help)}`);
}
console.log(JSON.stringify({ ok: true, size: metadata.size, entryCount: metadata.entryCount, version: pkg.version }));
} finally {
await fs.rm(temporary, { recursive: true, force: true });
}
async function exists(target) {
return fs.access(target).then(() => true, () => false);
}
function run(command, args, cwd, env = process.env, allowed = [0]) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, { cwd, env, shell: false, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
let stdout = "";
let stderr = "";
child.stdout.setEncoding("utf8");
child.stderr.setEncoding("utf8");
child.stdout.on("data", (chunk) => { stdout += chunk; });
child.stderr.on("data", (chunk) => { stderr += chunk; });
child.once("error", reject);
child.once("close", (code) => allowed.includes(code) ? resolve({ stdout, stderr, code }) : reject(new Error(`${command} ${args.join(" ")} failed (${code})\n${stderr}`)));
});
}
@@ -0,0 +1,82 @@
#!/usr/bin/env node
import { createHash } from "node:crypto";
import { constants as fsConstants, createReadStream } from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import {
RUNTIME_COMPONENT_DEPENDENCIES,
runtimeReleaseComponentForPath,
} from "../../runtime/install.js";
const [activeInput, outputInput] = process.argv.slice(2);
if (!activeInput || !outputInput) {
console.error("Usage: stage-runtime-components.mjs <active-runtime> <output-dir>");
process.exit(2);
}
const active = await fs.realpath(path.resolve(activeInput));
const output = path.resolve(outputInput);
const manifest = JSON.parse(await fs.readFile(path.join(active, ".gstack-bundle.json"), "utf8"));
if (manifest?.schemaVersion !== 2 || !Array.isArray(manifest.files)) {
throw new Error("Active runtime has no supported bundle manifest");
}
const componentIds = Object.keys(RUNTIME_COMPONENT_DEPENDENCIES).sort();
const summary = Object.fromEntries(componentIds.map((id) => [id, { files: 0, bytes: 0 }]));
for (const id of componentIds) await fs.mkdir(path.join(output, id, "gstack"), { recursive: true, mode: 0o700 });
for (const file of manifest.files) {
if (!file || typeof file.path !== "string" || !/^[a-f0-9]{64}$/.test(file.sha256) ||
!Number.isSafeInteger(file.size) || file.size < 0) {
throw new Error("Bundle manifest contains an invalid file record");
}
const relative = normalizeRelative(file.path);
const component = runtimeReleaseComponentForPath(relative);
if (component == null) continue; // Playwright's local GC bookkeeping is not runtime input.
if (!summary[component]) throw new Error(`No release component declared for ${relative}`);
const source = safeJoin(active, relative);
const stat = await fs.lstat(source);
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`Unsafe runtime source file: ${relative}`);
if (stat.size !== file.size || await sha256File(source) !== file.sha256) {
throw new Error(`Runtime file changed before component staging: ${relative}`);
}
const destination = safeJoin(path.join(output, component, "gstack"), relative);
await fs.mkdir(path.dirname(destination), { recursive: true, mode: 0o700 });
await fs.copyFile(source, destination, fsConstants.COPYFILE_EXCL);
if (process.platform !== "win32") await fs.chmod(destination, stat.mode & 0o777);
summary[component].files += 1;
summary[component].bytes += stat.size;
}
for (const [component, details] of Object.entries(summary)) {
if (details.files === 0) throw new Error(`Release component is empty: ${component}`);
}
await fs.writeFile(path.join(output, "components.json"), `${JSON.stringify(summary, null, 2)}\n`, { flag: "wx", mode: 0o644 });
process.stdout.write(`${JSON.stringify(summary)}\n`);
function normalizeRelative(value) {
const normalized = path.posix.normalize(value.replaceAll("\\", "/"));
if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../") || path.posix.isAbsolute(normalized)) {
throw new Error(`Unsafe manifest path: ${value}`);
}
return normalized;
}
function safeJoin(root, relative) {
const target = path.resolve(root, ...relative.split("/"));
const relation = path.relative(root, target);
if (relation === ".." || relation.startsWith(`..${path.sep}`) || path.isAbsolute(relation)) {
throw new Error(`Path escaped component root: ${relative}`);
}
return target;
}
function sha256File(file) {
return new Promise((resolve, reject) => {
const hash = createHash("sha256");
const stream = createReadStream(file);
stream.on("data", (chunk) => hash.update(chunk));
stream.once("error", reject);
stream.once("end", () => resolve(hash.digest("hex")));
});
}