fix(runner): use pinned npm for explicit Pi setup

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-01 23:52:29 -05:00
1 parent c9de5292d6
commit 8ced6f9285
2 files changed
+39 -5

No files matched your search

@@ -109,6 +109,19 @@ export function piDistributionBootstrapSource() {
].join("\n");
}
/** The already hash-verified Node archive also pins setup's package manager. */
export async function resolvePiBundledNpm(nodeRoot) {
const npmRoot = join(nodeRoot, "lib/node_modules/npm");
const manifest = join(npmRoot, "package.json");
const entry = join(npmRoot, "bin/npm-cli.js");
for (const path of [manifest, entry]) {
const info = await lstat(path);
if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1 || await realpath(path) !== path) throw new Error("Pinned Pi npm has an invalid archive entry");
}
if (JSON.parse(await readFile(manifest, "utf8")).version !== "11.19.0") throw new Error("Pinned Pi Node archive has an unexpected npm version");
return entry;
}
/** Build on the target platform. No lifecycle scripts, model requests, or auth. */
export async function materializePiDistribution({ outputRoot, nodeExecutable, npmExecutable = "npm", inputs, checkCancelled = () => {} }) {
// Cancellation is observed between bounded subprocesses. A setup signal must
@@ -131,6 +144,7 @@ export async function materializePiDistribution({ outputRoot, nodeExecutable, np
const target = `${process.platform}-${process.arch}`;
const nodePin = PI_NODE_DISTRIBUTIONS[target];
let node;
let bundledNpm;
if (nodeExecutable) node = await realpath(nodeExecutable);
else {
const archiveName = `node-v${PI_NODE_VERSION}-${target}.tar.gz`;
@@ -145,8 +159,9 @@ export async function materializePiDistribution({ outputRoot, nodeExecutable, np
if (hash(bytes) !== nodePin.archiveSha256) throw new Error("Pi Node archive does not match its release pin");
const archivePath = join(staging, archiveName); await writeFile(archivePath, bytes);
const nodeRoot = join(staging, "node-extract"); await mkdir(nodeRoot);
await runOwned("tar", ["-xzf", archivePath, "-C", nodeRoot, "--strip-components=2", `node-v${PI_NODE_VERSION}-${target}/bin/node`], { timeout: 30_000 });
node = join(nodeRoot, "node");
await runOwned("tar", ["-xzf", archivePath, "-C", nodeRoot, "--strip-components=1", `node-v${PI_NODE_VERSION}-${target}/bin/node`, `node-v${PI_NODE_VERSION}-${target}/lib/node_modules/npm`], { timeout: 30_000 });
node = join(nodeRoot, "bin/node");
bundledNpm = await resolvePiBundledNpm(nodeRoot);
}
if (hash(await readFile(node)) !== nodePin.executableSha256 || (await lstat(node)).size !== nodePin.executableSize) throw new Error("Pi Node executable does not match its target release pin");
const version = (await runOwned(node, ["--version"], { env: {}, timeout: buildNodeStartupTimeout() })).stdout.trim();
@@ -161,7 +176,10 @@ export async function materializePiDistribution({ outputRoot, nodeExecutable, np
// .npmrc. Public registry downloads need no private application credential.
const environment = Object.fromEntries(["PATH", "LANG", "LC_ALL", "HTTPS_PROXY", "HTTP_PROXY", "NO_PROXY", "SSL_CERT_FILE", "SSL_CERT_DIR", "NODE_EXTRA_CA_CERTS"].flatMap((key) => typeof process.env[key] === "string" ? [[key, process.env[key]]] : []));
environment.HOME = buildHome;
await runOwned(npmExecutable, piDistributionInstallCommand(), { cwd: runtimeRoot, env: environment, timeout: 300_000, maxBuffer: 4 * 1024 * 1024 });
// npm 10 prunes non-host packages bundled by upstream Pi, unlike the npm
// 11.19.0 used to qualify this complete closure. Public setup must use the
// npm pinned by the verified Node archive, never whichever npm is on PATH.
await runOwned(bundledNpm ? node : npmExecutable, [...(bundledNpm ? [bundledNpm] : []), ...piDistributionInstallCommand()], { cwd: runtimeRoot, env: environment, timeout: 300_000, maxBuffer: 4 * 1024 * 1024 });
const installedLockBytes = await readFile(join(runtimeRoot, "package-lock.json"));
if (!installedLockBytes.equals(await readFile(join(inputLockDirectory, "package-lock.json")))) throw new Error("Pi installation changed its committed lock");
const lock = JSON.parse(installedLockBytes.toString("utf8"));
@@ -1,10 +1,10 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { mkdtemp, mkdir, readFile, readdir, rm, symlink, writeFile } from "node:fs/promises";
import { mkdtemp, mkdir, readFile, readdir, realpath, rm, symlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import test from "node:test";
import { assertPiNodeSystemDependencies, PI_DISTRIBUTION_PINS, materializePiDistribution, piDistributionInstallCommand, verifyLockedPiPackageGraph, writePiDistributionManifest } from "./materialize-pi-distribution.mjs";
import { assertPiNodeSystemDependencies, PI_DISTRIBUTION_PINS, materializePiDistribution, resolvePiBundledNpm, piDistributionInstallCommand, verifyLockedPiPackageGraph, writePiDistributionManifest } from "./materialize-pi-distribution.mjs";
import { verifyPiRuntimeManifest } from "../src/drivers/acpx/pi-verified-runtime.ts";
async function fixture(t) {
@@ -98,3 +98,19 @@ test("Node dependency inspection rejects Homebrew and non-system Linux libraries
assert.throws(() => assertPiNodeSystemDependencies("libnode.so => /opt/lib/libnode.so (0x000)\n", "linux"), /unbundled/);
assert.doesNotThrow(() => assertPiNodeSystemDependencies("linux-vdso.so.1 (0x000)\nlibc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x000)\n/lib64/ld-linux-x86-64.so.2 (0x000)\n", "linux"));
});
test("setup selects the exact archive npm and rejects wrong versions or linked entrypoints", async (t) => {
const fixtureRoot = await fixture(t);
const root = await realpath(fixtureRoot.root), write = fixtureRoot.write;
const prefix = "lib/node_modules/npm/";
await write(prefix + "package.json", JSON.stringify({ name: "npm", version: "11.19.0" }));
await write(prefix + "bin/npm-cli.js", "// pinned archive fixture");
assert.equal(await resolvePiBundledNpm(root), join(root, prefix, "bin/npm-cli.js"));
await write(prefix + "package.json", JSON.stringify({ name: "npm", version: "10.9.7" }));
await assert.rejects(resolvePiBundledNpm(root), /unexpected npm version/);
await write(prefix + "package.json", JSON.stringify({ name: "npm", version: "11.19.0" }));
await rm(join(root, prefix, "bin/npm-cli.js"));
await symlink(join(root, prefix, "package.json"), join(root, prefix, "bin/npm-cli.js"));
await assert.rejects(resolvePiBundledNpm(root), /invalid archive entry/);
});