fix(runner): pin patched Undici and Node in Pi profile v5

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-28 17:33:50 -05:00
1 parent 6a4785d32f
commit aec26ad83f
12 files changed
+216 -43

No files matched your search

+41 -14
View File
@@ -1,9 +1,10 @@
# Pi rich ACP runtime
Status: implementation candidate, 2026-09-28. Profile version 4 repairs native
tool ID reuse across model iterations and warm prompts. Its deterministic bridge,
SDK ordering, lifecycle and history regressions pass; authenticated v4
qualification is pending. Historical version 3 passes cover local Product file
Status: implementation candidate, 2026-09-28. Profile version 5 repairs the
Undici dependency and bundled Node runtime affected by GHSA-3wwx-pv8p-q78v.
Authenticated v5 qualification is pending. Historical version 4 repairs native
tool ID reuse across model iterations and warm prompts; its native steering,
queued follow-up and denied-write probe passes. Historical version 3 passes cover local Product file
delivery, typed question continuation and native steering, queued follow-up and
denied-write controls. Its semantic plan journey exposed two defects: the shared
MCP display projection, now repaired, followed by a resumed-turn ID collision,
@@ -81,10 +82,11 @@ cached validation errors, concurrent exact retries, changed-payload rejection,
and a corrected invocation in the next iteration. Installed-wrapper tests verify
lifecycle correlation and stable, disjoint historical replay identities.
The v4 command digest is
`sha256:2324d9b47650c12b16f8e2c44dc33637d52f1b22ba8e914623eac4049e7e1991`.
Versions 1, 2 and 3 cannot reopen under this identity. Older paid and image proofs
below remain evidence of their recorded versions, not v4 qualification.
The current v5 command digest is
`sha256:020d96ccbd3c45c3f62680814776394ed5a56d9572a1a4dccda56a74d16c7803`.
Versions 1–4 cannot reopen under this identity. Paid and image proofs below remain
evidence of their recorded versions, not v5 qualification. The historical v4
digest is `sha256:2324d9b47650c12b16f8e2c44dc33637d52f1b22ba8e914623eac4049e7e1991`.
## Lifetime and recovery
@@ -221,7 +223,7 @@ Primary references reviewed:
Run `node packages/paperclip-runner/scripts/materialize-pi-distribution.mjs
/absolute/new-output` on the target build host. Supported targets are macOS arm64,
macOS x64 and Linux x64; no cross-platform qualification is inferred. The builder
uses exact official Node 24.19.0, pinned archive SHA-256 and executable SHA-256
uses exact official Node 24.21.0, pinned archive SHA-256 and executable SHA-256
for each target. `--node=/absolute/portable-node` reuses a matching binary; without
it the builder downloads the pinned official archive. It rejects non-system
dynamic-library dependencies and executes the copied interpreter after relocation.
@@ -234,10 +236,34 @@ preserves Pi's upstream nested shrinkwrap, and adds npm registry SHA-512 integri
for the six exact 0.84.2 Pi family packages whose published shrinkwrap omitted it.
Installation uses `npm ci --ignore-scripts`, public registry access, private npm
configuration and an environment without npm or provider credentials. The builder
checks every locked installed version and upstream shrinkwrap entry, applies the
checks every locked installed version and upstream shrinkwrap entry (with the
exact security exception below), applies the
owned ACP patch, verifies the helper matches its TypeScript source, and compiles
the owned extension to `runtime/extensions/paperclip.js`.
Profile v5 replaces only the nested `undici@8.9.0` with `8.10.2`. Both an
existing-lock resolution and a fresh resolution ignore npm's scoped override
inside Pi's published shrinkwrap; `npm ci` also installs 8.9.0 despite the fixed
outer lock. The materializer therefore checks the exact original nested path,
version, registry URL and SHA-512, plus the installed vulnerable package identity,
before fetching the exact fixed tarball. It verifies the pinned SHA-512 before
extraction, bounds both gzip and inflated tar bytes to 4 MiB, rejects traversal,
links and unexpected entry types, and requires the 214 regular-file payload.
The published Pi package metadata and shrinkwrap remain unchanged; only this exact
old-to-new tuple is an exception. Every other package retains its previous lock
entry. Installed version checks and the complete source-pinned closure then make
it impossible for npm's old copy to survive admission.
Node 24.21.0 also replaces bundled Undici 7.29.0 with 7.29.1, covering the global
WebSocket implementation as well as the nested npm copy. The materializer checks
`process.versions.undici` explicitly. Sources: [official advisory](https://github.com/advisories/GHSA-3wwx-pv8p-q78v),
[fixed Undici release](https://github.com/nodejs/undici/releases/tag/v8.10.2),
[Node 24.21.0 release](https://nodejs.org/en/blog/release/v24.21.0) and
[official archive checksums](https://nodejs.org/dist/v24.21.0/SHASUMS256.txt).
Only Pi's declarations and three closure pins change; sibling provider pins
remain unchanged. The containing provider pack must be rebuilt with the patched
portable interpreter and retains its own manifest identity.
The result contains `runtime/` and a sibling `pi-distribution.json`. Keeping the
manifest outside its inventoried root avoids a self-referential file digest. The
returned environment bindings point into `runtime/`; callers must retain a verified
@@ -295,7 +321,7 @@ The first authenticated local Runner snapshot verified the exact configured
model ID. It did not settle or produce a terminal usage receipt; complete local
and Linux/Daytona receipt qualification remains outstanding.
Historical profile version 3 declaration digest: `sha256:72cb225288376f733b9ed3afa5e13565eb4152f0de509bc1181382fa44bee472`. The current v4 digest above hashes its versioned
Historical profile version 3 declaration digest: `sha256:72cb225288376f733b9ed3afa5e13565eb4152f0de509bc1181382fa44bee472`. The current v5 digest above hashes its versioned
profile domain, patched wrapper source and platform closure pins. Every native
closure remains independently checked at launch. Version 1, 2 and 3 warm sessions cannot
be reused with this integration.
@@ -418,9 +444,10 @@ attempt at $0.000546502; Pi's separate catalog estimate is $0.000803936. Initial
unchanged billing reads are retained and were not treated as free inference.
A newly indexed [Undici advisory](https://github.com/advisories/GHSA-3wwx-pv8p-q78v)
also affects the current isolated npm Undici 8.9.0 and Node 24.19.0's bundled
Undici 7.29.0. Further paid work is held while the provider-local v5 dependency
repair is prepared. This paid v4 proof remains bound to its original bytes.
affects the historical v4 isolated npm Undici 8.9.0 and Node 24.19.0's bundled
Undici 7.29.0. Profile v5 fixes both copies as described above. Further paid work
remains held until fresh pack proofs and a new bounded authorization. This paid
v4 proof remains bound to its original bytes.
The first [v4 plan attempt](../../packages/paperclip-runner/test-fixtures/pi-acp/product-plan-infrastructure-failure.v4.darwin-arm64.json)
failed before provider startup: embedded PostgreSQL could not initialize the
@@ -170,7 +170,7 @@ impl AcpxProviderDescriptor {
"0.0.33",
Some("@earendil-works/pi-coding-agent"),
Some("0.84.2"),
"sha256:2324d9b47650c12b16f8e2c44dc33637d52f1b22ba8e914623eac4049e7e1991",
"sha256:020d96ccbd3c45c3f62680814776394ed5a56d9572a1a4dccda56a74d16c7803",
),
"cursor" => (
self.model.as_str(),
@@ -2461,7 +2461,7 @@ mod tests {
"pi",
"pi-acp",
"0.0.33",
"sha256:2324d9b47650c12b16f8e2c44dc33637d52f1b22ba8e914623eac4049e7e1991",
"sha256:020d96ccbd3c45c3f62680814776394ed5a56d9572a1a4dccda56a74d16c7803",
Some("@earendil-works/pi-coding-agent"),
Some("0.84.2"),
"openrouter/deepseek/deepseek-v4-flash-0731",
@@ -17,7 +17,7 @@ use sha2::{Digest, Sha256};
const CODEX_ACPX_DIGEST: &str =
"sha256:c4538599d1ab767db5dff50934f13bb5ba313a59d9c4a83e993fac4617ea63d3";
const PI_ACPX_DIGEST: &str =
"sha256:2324d9b47650c12b16f8e2c44dc33637d52f1b22ba8e914623eac4049e7e1991";
"sha256:020d96ccbd3c45c3f62680814776394ed5a56d9572a1a4dccda56a74d16c7803";
fn temporary_directory(label: &str) -> PathBuf {
let nonce = SystemTime::now()
@@ -5,6 +5,7 @@ import { stripTypeScriptTypes } from "node:module";
import { dirname, isAbsolute, join, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { promisify } from "node:util";
import { gunzipSync } from "node:zlib";
import { PI_DISTRIBUTION_CLOSURE_SHA256 } from "../src/drivers/acpx/pi-closure-pins.ts";
import { PI_NODE_DISTRIBUTIONS, PI_NODE_VERSION } from "../src/drivers/acpx/pi-node-pins.ts";
import { QUALIFIED_ACPX_PROFILES } from "../src/drivers/acpx/qualified-profiles.ts";
@@ -17,13 +18,79 @@ const lockDirectory = join(packageRoot, "scripts/pi-distribution");
const patchPath = join(workspaceRoot, "patches/pi-acp@0.0.33.patch");
const supportedTargets = new Set(["darwin-arm64", "darwin-x64", "linux-x64"]);
export const PI_DISTRIBUTION_PINS = Object.freeze({
wrapper: "0.0.33", runtime: "0.84.2", sdk: "0.26.0", zod: "3.25.76", nodeVersion: PI_NODE_VERSION,
wrapper: "0.0.33", runtime: "0.84.2", sdk: "0.26.0", zod: "3.25.76", nodeVersion: PI_NODE_VERSION, undici: "8.10.2", nodeBundledUndici: "7.29.1",
wrapperSha256: "dc4786faff30942e82106c87a8984a75fb979a925ff1d62648500a642b920cac",
helperSha256: "9e50b60644d0d00b3fb2660eb78ad616dd01395775bcb05974d2bdeadc7f7e57",
});
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
/** npm ci honors the committed graph and Pi's published nested shrinkwrap. */
/** The sole reviewed departure from Pi 0.84.2's published shrinkwrap.
* npm ci reifies that upstream shrinkwrap even when the outer lock and override
* request the fixed version. Keep the published metadata intact, replace this
* exact package explicitly, and bind the resulting files in all closure pins.
* https://github.com/advisories/GHSA-3wwx-pv8p-q78v
*/
export const PI_UNDICI_SECURITY_OVERRIDE = Object.freeze({
path: "node_modules/@earendil-works/pi-coding-agent/node_modules/undici",
upstreamPath: "node_modules/undici",
previous: Object.freeze({ version: "8.9.0", resolved: "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz", integrity: "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==" }),
fixed: Object.freeze({ version: "8.10.2", resolved: "https://registry.npmjs.org/undici/-/undici-8.10.2.tgz", integrity: "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==" }),
});
const samePackagePin = (entry, pin) => entry?.version === pin.version && entry?.resolved === pin.resolved && entry?.integrity === pin.integrity;
export function assertPiUndiciSecurityOverride(lock, shrinkwrap) {
const pin = PI_UNDICI_SECURITY_OVERRIDE;
if (shrinkwrap.version !== PI_DISTRIBUTION_PINS.runtime || shrinkwrap.lockfileVersion !== 3 ||
!samePackagePin(shrinkwrap.packages?.[pin.upstreamPath], pin.previous) ||
!samePackagePin(lock.packages?.[pin.path], pin.fixed)) throw new Error("Pi Undici security override differs from its exact reviewed old/new pins");
}
export function assertPiUndiciArchive(bytes) {
if (bytes.length > 4 * 1024 * 1024 || `sha512-${createHash("sha512").update(bytes).digest("base64")}` !== PI_UNDICI_SECURITY_OVERRIDE.fixed.integrity) throw new Error("Pi fixed Undici archive differs from its integrity pin");
// Bound the complete tar stream (headers and file payloads), not just gzip.
gunzipSync(bytes, { maxOutputLength: 4 * 1024 * 1024 });
}
export function assertPiUndiciArchiveEntries(paths, listing) {
const entries = paths.trim().split("\n");
if (entries.length < 2 || entries.length > 512 || entries.some(path => !path.startsWith("package/") || path.includes("\\") || path.split("/").some(part => part === ".." || part === ".") || /[\x00-\x1f\x7f]/.test(path)) ||
listing.trim().split("\n").length !== entries.length || listing.trim().split("\n").some(line => !/^[d-]/.test(line))) throw new Error("Pi fixed Undici archive has an unsafe entry");
}
export async function applyPiUndiciSecurityOverride(root, lock) {
root = await realpath(root);
const pin = PI_UNDICI_SECURITY_OVERRIDE;
const piRoot = join(root, "node_modules/@earendil-works/pi-coding-agent");
const shrinkwrap = JSON.parse(await readFile(join(piRoot, "npm-shrinkwrap.json"), "utf8"));
assertPiUndiciSecurityOverride(lock, shrinkwrap);
const installed = join(root, pin.path);
const metadata = JSON.parse(await readFile(join(installed, "package.json"), "utf8"));
if ((await lstat(installed)).isSymbolicLink() || await realpath(installed) !== installed || metadata.name !== "undici" || metadata.version !== pin.previous.version) throw new Error("Pi npm install did not produce the expected upstream Undici package");
const temporary = await mkdtemp(join(dirname(installed), ".paperclip-undici-fix-"));
try {
const response = await fetch(pin.fixed.resolved, { redirect: "error", signal: AbortSignal.timeout(60_000) });
if (!response.ok || !response.body) throw new Error("Pi fixed Undici download failed");
const chunks = []; let length = 0;
for await (const chunk of response.body) {
length += chunk.length; if (length > 4 * 1024 * 1024) throw new Error("Pi fixed Undici archive exceeds its bound");
chunks.push(Buffer.from(chunk));
}
const bytes = Buffer.concat(chunks); assertPiUndiciArchive(bytes);
const archive = join(temporary, "fixed.tgz"); await writeFile(archive, bytes);
const options = { env: { PATH: "/usr/bin:/bin" }, timeout: 30_000, maxBuffer: 1024 * 1024 };
const [paths, listing] = await Promise.all([run("tar", ["-tzf", archive], options), run("tar", ["-tvzf", archive], options)]);
assertPiUndiciArchiveEntries(paths.stdout, listing.stdout);
const extracted = join(temporary, "package"); await mkdir(extracted);
await run("tar", ["-xzf", archive, "-C", extracted, "--strip-components=1", "--no-same-owner"], options);
const fixed = JSON.parse(await readFile(join(extracted, "package.json"), "utf8"));
if (fixed.name !== "undici" || fixed.version !== pin.fixed.version) throw new Error("Pi fixed Undici package identity is invalid");
const files = await inventoryPiRuntimeFiles(extracted);
if (files.length !== 214 || files.some(file => file.kind !== "file")) throw new Error("Pi fixed Undici package payload is invalid");
await rm(installed, { recursive: true });
await rename(extracted, installed);
} finally { await rm(temporary, { recursive: true, force: true }); }
}
/** npm ci honors Pi's published nested shrinkwrap; the exact security fix follows. */
export function piDistributionInstallCommand() {
return ["ci", "--ignore-scripts", "--include=optional", "--omit=dev", "--no-audit", "--no-fund", "--registry=https://registry.npmjs.org", "--userconfig=.npmrc", "--globalconfig=.npmrc-global"];
}
@@ -55,6 +122,7 @@ export async function verifyLockedPiPackageGraph(root, lock, target = { platform
for (const [path, entry] of Object.entries(shrinkwrap.packages)) {
if (!path) continue;
const pinned = lock.packages[`${piPath}/${path}`];
if (path === PI_UNDICI_SECURITY_OVERRIDE.upstreamPath) { assertPiUndiciSecurityOverride(lock, shrinkwrap); continue; }
if (!pinned || pinned.version !== entry.version || (entry.integrity !== undefined && pinned.integrity !== entry.integrity) || pinned.resolved !== entry.resolved) throw new Error(`Pi distribution dropped or changed shrinkwrapped package ${path}`);
}
return verified;
@@ -142,6 +210,7 @@ export async function materializePiDistribution({ outputRoot, nodeExecutable, np
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 run(node, ["--version"], { env: {}, timeout: 10_000 })).stdout.trim();
if (version !== `v${PI_NODE_VERSION}`) throw new Error("Pi distribution requires exact pinned Node version");
if ((await run(node, ["-p", "process.versions.undici"], { env: {}, timeout: 10_000 })).stdout.trim() !== PI_DISTRIBUTION_PINS.nodeBundledUndici) throw new Error("Pi Node bundled Undici differs from its reviewed security pin");
await mkdir(runtimeRoot);
await Promise.all(["package.json", "package-lock.json"].map((name) => copyFile(join(lockDirectory, name), join(runtimeRoot, name))));
await writeFile(join(runtimeRoot, ".npmrc"), "registry=https://registry.npmjs.org/\nignore-scripts=true\naudit=false\nfund=false\n");
@@ -155,6 +224,7 @@ export async function materializePiDistribution({ outputRoot, nodeExecutable, np
const installedLockBytes = await readFile(join(runtimeRoot, "package-lock.json"));
if (!installedLockBytes.equals(await readFile(join(lockDirectory, "package-lock.json")))) throw new Error("Pi installation changed its committed lock");
const lock = JSON.parse(installedLockBytes.toString("utf8"));
await applyPiUndiciSecurityOverride(runtimeRoot, lock);
const packageCount = await verifyLockedPiPackageGraph(runtimeRoot, lock);
const wrapper = join(runtimeRoot, "node_modules/pi-acp");
await run("git", ["apply", "--check", patchPath], { cwd: wrapper, env: environment, timeout: 10_000 });
@@ -1,9 +1,9 @@
import assert from "node:assert/strict";
import { mkdtemp, mkdir, readFile, rm, symlink, writeFile } from "node:fs/promises";
import { mkdtemp, mkdir, readFile, readdir, 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, applyPiUndiciSecurityOverride, assertPiUndiciArchive, assertPiUndiciArchiveEntries, assertPiUndiciSecurityOverride, PI_UNDICI_SECURITY_OVERRIDE, PI_DISTRIBUTION_PINS, materializePiDistribution, piDistributionInstallCommand, verifyLockedPiPackageGraph, writePiDistributionManifest } from "./materialize-pi-distribution.mjs";
import { verifyPiRuntimeManifest } from "../src/drivers/acpx/pi-verified-runtime.ts";
async function fixture(t) {
@@ -17,6 +17,10 @@ test("distribution lock closes exact wrapper, SDK and upstream Pi graph with int
const pkg = JSON.parse(await readFile(new URL("./pi-distribution/package.json", import.meta.url), "utf8"));
const lock = JSON.parse(await readFile(new URL("./pi-distribution/package-lock.json", import.meta.url), "utf8"));
assert.deepEqual(lock.packages[""].dependencies, pkg.dependencies);
assert.deepEqual(pkg.overrides, { "@earendil-works/pi-coding-agent@0.84.2": { undici: "8.10.2" } });
assert.equal(lock.packages[PI_UNDICI_SECURITY_OVERRIDE.path].version, "8.10.2");
assert.equal(PI_DISTRIBUTION_PINS.nodeVersion, "24.21.0");
assert.equal(PI_DISTRIBUTION_PINS.nodeBundledUndici, "7.29.1");
assert.deepEqual(pkg.dependencies, { "@agentclientprotocol/sdk": "0.26.0", "@earendil-works/pi-coding-agent": "0.84.2", "pi-acp": "0.0.33", zod: "3.25.76" });
for (const [path, entry] of Object.entries(lock.packages)) {
if (!path) continue;
@@ -82,3 +86,68 @@ 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("security exception admits only the exact upstream-to-fixed Undici tuple", async (t) => {
const { root, write } = await fixture(t);
const pi = "node_modules/@earendil-works/pi-coding-agent";
const pin = PI_UNDICI_SECURITY_OVERRIDE;
const lock = { lockfileVersion: 3, packages: {
[pi]: { version: "0.84.2", resolved: "https://registry.npmjs.org/pi.tgz", integrity: "sha512-YWJjZA==" },
[pin.path]: { ...pin.fixed },
} };
const shrinkwrap = { version: "0.84.2", lockfileVersion: 3, packages: { [pin.upstreamPath]: { ...pin.previous } } };
await write(`${pi}/package.json`, JSON.stringify({ version: "0.84.2" }));
await write(`${pi}/npm-shrinkwrap.json`, JSON.stringify(shrinkwrap));
// npm ci's vulnerable copy cannot pass the post-replacement installed check.
await write(`${pin.path}/package.json`, JSON.stringify({ name: "undici", version: "8.9.0" }));
await assert.rejects(verifyLockedPiPackageGraph(root, lock), /differs from its lock/);
await write(`${pin.path}/package.json`, JSON.stringify({ name: "undici", version: "8.10.2" }));
assert.equal(await verifyLockedPiPackageGraph(root, lock), 2);
for (const field of ["version", "resolved", "integrity"]) {
const badOld = structuredClone(shrinkwrap); badOld.packages[pin.upstreamPath][field] = "tampered";
assert.throws(() => assertPiUndiciSecurityOverride(lock, badOld), /exact reviewed old\/new pins/);
const badNew = structuredClone(lock); badNew.packages[pin.path][field] = "tampered";
assert.throws(() => assertPiUndiciSecurityOverride(badNew, shrinkwrap), /exact reviewed old\/new pins/);
}
const moved = structuredClone(lock); moved.packages["node_modules/undici"] = moved.packages[pin.path]; delete moved.packages[pin.path];
assert.throws(() => assertPiUndiciSecurityOverride(moved, shrinkwrap), /exact reviewed/);
const altered = structuredClone(shrinkwrap); altered.packages[pin.upstreamPath].integrity = "sha512-YWJjZA==";
await write(`${pi}/npm-shrinkwrap.json`, JSON.stringify(altered));
await assert.rejects(verifyLockedPiPackageGraph(root, lock), /exact reviewed/);
});
test("security tarball rejects tampering, traversal, links and unbounded entry lists", () => {
assert.throws(() => assertPiUndiciArchive(Buffer.from("not the pinned tarball")), /integrity pin/);
assert.throws(() => assertPiUndiciArchive(Buffer.alloc(4 * 1024 * 1024 + 1)), /integrity pin/);
const goodPaths = "package/package.json\npackage/index.js\n";
const goodListing = "-rw-r--r-- package/package.json\n-rw-r--r-- package/index.js\n";
assert.doesNotThrow(() => assertPiUndiciArchiveEntries(goodPaths, goodListing));
for (const path of ["/absolute", "package/../escape", "other/index.js", "package/./index.js", "package/evil\\name"]) {
assert.throws(() => assertPiUndiciArchiveEntries(`package/package.json\n${path}\n`, goodListing), /unsafe entry/);
}
assert.throws(() => assertPiUndiciArchiveEntries(goodPaths, goodListing.replace("-rw", "lrw")), /unsafe entry/);
assert.throws(() => assertPiUndiciArchiveEntries(goodPaths, goodListing.replace("-rw", "hrw")), /unsafe entry/);
assert.throws(() => assertPiUndiciArchiveEntries(goodPaths.repeat(257), goodListing.repeat(257)), /unsafe entry/);
});
test("tampered replacement leaves the original dependency intact and cleans staging", async (t) => {
const { root, write } = await fixture(t);
const pin = PI_UNDICI_SECURITY_OVERRIDE;
const pi = "node_modules/@earendil-works/pi-coding-agent";
const lock = { packages: { [pin.path]: { ...pin.fixed } } };
await write(`${pi}/npm-shrinkwrap.json`, JSON.stringify({ version: "0.84.2", lockfileVersion: 3, packages: { [pin.upstreamPath]: { ...pin.previous } } }));
const original = JSON.stringify({ name: "undici", version: "8.9.0" });
await write(`${pin.path}/package.json`, original);
const savedFetch = globalThis.fetch;
t.after(() => { globalThis.fetch = savedFetch; });
globalThis.fetch = async (url, options) => {
assert.equal(url, pin.fixed.resolved);
assert.equal(options.redirect, "error");
return new Response("tampered archive");
};
await assert.rejects(applyPiUndiciSecurityOverride(root, lock), /integrity pin/);
assert.equal(await readFile(join(root, pin.path, "package.json"), "utf8"), original);
assert.deepEqual(await readdir(join(root, pi, "node_modules")), ["undici"]);
});
@@ -1755,9 +1755,9 @@
"license": "MIT"
},
"node_modules/@earendil-works/pi-coding-agent/node_modules/undici": {
"version": "8.9.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz",
"integrity": "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==",
"version": "8.10.2",
"resolved": "https://registry.npmjs.org/undici/-/undici-8.10.2.tgz",
"integrity": "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==",
"license": "MIT",
"engines": {
"node": ">=22.19.0"
@@ -11,5 +11,10 @@
},
"engines": {
"node": ">=24.11.0"
},
"overrides": {
"@earendil-works/pi-coding-agent@0.84.2": {
"undici": "8.10.2"
}
}
}
@@ -1,12 +1,12 @@
/**
* Candidate closure pins from the isolated npm lock and official Node 24.19.0.
* Candidate closure pins from the isolated npm lock and official Node 24.21.0.
* The non-Node graph is identical across targets, including platform resources.
* macOS arm64 executed admission tests; x64 target execution remains pending.
* Changing any package, helper, extension or bootstrap requires regenerating all
* three pins. Never accept a digest supplied only by an installed manifest.
*/
export const PI_DISTRIBUTION_CLOSURE_SHA256 = Object.freeze({
"darwin-arm64": "b0e02ed16d27fd1d90a5e91d6309f1c47d6cf55f8937a8f680baeee19646d320",
"darwin-x64": "a79b55d7773f4f67d1c6d87fe67346852df5a23e36e8c55d1e84f80d4b668f22",
"linux-x64": "fc7464cb5ea5af2a2d2519f3a67f7471ca3185110f33eaaea3785043709ad3b2",
"darwin-arm64": "0010175e4bc200f145386e59f7a824cf7532ad273e9db6824d22c4f82cdcd6ff",
"darwin-x64": "9dd49bd1341a6e3438db56cc6b3dc772964ebcdd0552bcfbf2e5e621b2970ab6",
"linux-x64": "91169667544764764e2e402868fbb76a9a429575cfdb1b312b8ed8ae669ebc89",
});
@@ -10,7 +10,7 @@ import { QUALIFIED_ACPX_PROFILES, type QualifiedAcpxProfile } from "./qualified-
const roots: string[] = [];
afterEach(async () => { vi.unstubAllEnvs(); await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true }))); });
const candidate = (): QualifiedAcpxProfile => ({ ...QUALIFIED_ACPX_PROFILES.pi, agentProfileVersion: 4 });
const candidate = (): QualifiedAcpxProfile => ({ ...QUALIFIED_ACPX_PROFILES.pi, agentProfileVersion: 5 });
async function fixture() {
const root = await mkdtemp(join(tmpdir(), "pi-installation-test-")); roots.push(root);
@@ -24,7 +24,7 @@ async function fixture() {
describe("Pi installation factory", () => {
it("binds the profile declaration to the reviewed patch and platform closure pins", async () => {
const digest = createHash("sha256").update("paperclip.pi.rich-acp.profile.v4\0")
const digest = createHash("sha256").update("paperclip.pi.rich-acp.profile.v5\0")
.update(await readFile(new URL("../../../../../patches/pi-acp@0.0.33.patch", import.meta.url)))
.update("\0").update(await readFile(new URL("./pi-closure-pins.ts", import.meta.url))).digest("hex");
expect(QUALIFIED_ACPX_PROFILES.pi.commandDigest).toBe(`sha256:${digest}`);
@@ -32,9 +32,10 @@ describe("Pi installation factory", () => {
it("rejects legacy profiles and caller-selected identities", () => {
expect(() => assertPiInstallationProfile(candidate())).not.toThrow();
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 1 })).toThrow("version 4");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 2 })).toThrow("version 4");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 3 })).toThrow("version 4");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 1 })).toThrow("version 5");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 2 })).toThrow("version 5");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 3 })).toThrow("version 5");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 4 })).toThrow("version 5");
for (const changed of [{ agentServerVersion: "latest" }, { commandDigest: `sha256:${"0".repeat(64)}` }, { reportedModelId: "different" }, { agentRuntimePackage: "ambient-pi" }]) {
expect(() => assertPiInstallationProfile({ ...candidate(), ...changed })).toThrow("trusted declaration");
}
@@ -43,8 +44,8 @@ describe("Pi installation factory", () => {
const { assets } = await fixture();
await writeFile(join(assets, "pi-distribution.json"), JSON.stringify({
schema: "paperclip.pi-distribution.v1", runtimeRoot: "runtime", nativeClosureSha256: "0".repeat(64),
target: { platform: process.platform, architecture: process.arch, nodeVersion: "24.19.0" },
pins: { nodeVersion: "24.19.0", wrapper: "0.0.33", runtime: "0.84.2", sdk: "0.26.0", zod: "3.25.76" },
target: { platform: process.platform, architecture: process.arch, nodeVersion: "24.21.0" },
pins: { nodeVersion: "24.21.0", wrapper: "0.0.33", runtime: "0.84.2", sdk: "0.26.0", zod: "3.25.76" },
}));
await expect(verifyPiInstallation(candidate())).rejects.toThrow("trusted target pin");
});
@@ -24,7 +24,7 @@ function record(value: unknown): Record<string, unknown> {
/** Candidate identity is build-owned; model selection cannot substitute a CLI. */
export function assertPiInstallationProfile(profile: QualifiedAcpxProfile): void {
const trusted = QUALIFIED_ACPX_PROFILES.pi;
if (profile.agentProfileVersion !== 4) throw new Error("Pi rich ACP requires profile version 4; reopen the previous session");
if (profile.agentProfileVersion !== 5) throw new Error("Pi rich ACP requires profile version 5; reopen the previous session");
if (profile.agent !== "pi" || profile.driverKind !== trusted.driverKind || profile.protocolVersion !== trusted.protocolVersion || profile.acpxVersion !== trusted.acpxVersion || profile.agentServerPackage !== "pi-acp" || profile.agentServerVersion !== "0.0.33" || profile.agentRuntimePackage !== "@earendil-works/pi-coding-agent" || profile.agentRuntimeVersion !== "0.84.2" || profile.commandDigest !== trusted.commandDigest || profile.permissionPolicy !== "interactive" || profile.qualificationModel !== PI_MODEL || profile.reportedModelId !== PI_MODEL) throw new Error("Pi distribution profile differs from its trusted declaration");
}
@@ -1,7 +1,8 @@
/** Official Node release SHASUMS256.txt, version 24.19.0, reviewed 2026-09-28. */
export const PI_NODE_VERSION = "24.19.0" as const;
/** Official Node release SHASUMS256.txt, version 24.21.0, reviewed 2026-09-28.
* https://nodejs.org/dist/v24.21.0/SHASUMS256.txt — bundled Undici 7.29.1. */
export const PI_NODE_VERSION = "24.21.0" as const;
export const PI_NODE_DISTRIBUTIONS = Object.freeze({
"darwin-arm64": Object.freeze({ archiveSha256: "8294b7aa9b03997481c06babf1e8b270c859358f27da57a11509afe537ac381d", executableSha256: "27db838bb204ef7c21df2931f5656e4c8fb32e6e947f363a402b49714d32b5b1", executableSize: 121306800 }),
"darwin-x64": Object.freeze({ archiveSha256: "d1b5e999db158c62fe8f7267a4476b035d8bd93b1a605bac24a3f0dd166e3316", executableSha256: "1052eb9c7d6c60a79b968e09f75af55a73462b0f6dff0964336d63b5e13eb63c", executableSize: 123666640 }),
"linux-x64": Object.freeze({ archiveSha256: "f625d97cd707df4ff96254916fbc5ff014f09c09effe5a1e0ca8f6d41a8789d4", executableSha256: "bc17c508ffeed0ec622934f9b7fa72f8e78da65350e63c3eceb56fa688aa5e12", executableSize: 125989464 }),
"darwin-arm64": Object.freeze({ archiveSha256: "bed7eea5325e1108f32ce5228ddd6a5f0f08a499ee42aa7442aea583702f6057", executableSha256: "e4b5a3af0e05c75de2eae013904145f40fe7fc2a6e6f17510128bf45cca4e79b", executableSize: 122129232 }),
"darwin-x64": Object.freeze({ archiveSha256: "1462cb3b3046b815cf8ea436d3da450ec1a9f11dac7e5a46b0ada5305d7e8097", executableSha256: "7abcf39bd37ab251015337ff75304d7555f0d8e88c6e0fbf04bce8ce34636f49", executableSize: 125270960 }),
"linux-x64": Object.freeze({ archiveSha256: "6e1db87ef58b8819e5d5402eff1536491b18edd8eb7bee5ef7897876e88dc5ff", executableSha256: "7fde7b8afa198da66257f42ee2001d874c7355631e6d1579a5fb5ef1f246df4c", executableSize: 126595440 }),
});
@@ -47,14 +47,14 @@ export const QUALIFIED_ACPX_PROFILES: Readonly<
protocolVersion: ACPX_DRIVER_PROTOCOL_VERSION,
acpxVersion: QUALIFIED_ACPX_VERSION,
agent: "pi",
agentProfileVersion: 4,
agentProfileVersion: 5,
qualificationStatus: "pending",
agentServerPackage: "pi-acp",
agentServerVersion: "0.0.33",
agentRuntimePackage: "@earendil-works/pi-coding-agent",
agentRuntimeVersion: "0.84.2",
commandDigest:
"sha256:2324d9b47650c12b16f8e2c44dc33637d52f1b22ba8e914623eac4049e7e1991",
"sha256:020d96ccbd3c45c3f62680814776394ed5a56d9572a1a4dccda56a74d16c7803",
qualificationModel: "openrouter/deepseek/deepseek-v4-flash-0731",
reportedModelId: "openrouter/deepseek/deepseek-v4-flash-0731",
permissionPolicy: "interactive",