// The Pi pin and the engine seal (#1507, CHAT-03 ยง3, lead decisions 31โ€“32). // // Pin: package-lock.json and npm's installed record // (node_modules/.package-lock.json) must both name the pinned version with the // pinned integrity. That ties the install to the package through npm's record; // it is not a hash of the files on disk. `pi` runs dist/bundle/cli.js, the // package's bin, and the built-in llama.cpp extension ships inside it. // // Seal: the controller builds the launch argv. It always carries // --no-extensions, --no-prompt-templates and --no-themes, and never an // --extension argument (cli/args.js; usage.md 224 and 233โ€“236). With // --no-extensions Pi loads only command-line extension paths // (resource-loader.js 316โ€“318), so no explicit extension loads. Under the seal // the Mosaic prompt in the slot is the only thing that can start a run, which // is the basis for attributing a run to it by order. // // The seal is an allow-list. Pi's parser (cli/args.js) keeps the last --mode // and the last --session, reads a bare word as a prompt and an `@` word as a // file, so the argv must be exactly the controller's prefix followed by // ENGINE_OPTIONS pairs, each at most once with one plain value. import { readFileSync } from "node:fs"; import { isAbsolute, join } from "node:path"; import { createHash } from "node:crypto"; import { ControlRefusal } from "./safe-fs.mjs"; export const PI_PACKAGE = "@earendil-works/pi-coding-agent"; export const PI_VERSION = "0.85.1"; export const PI_INTEGRITY = "sha512-FGRN+OHbWaefBPGaTggAdLjrIHW+s2PzLyglz/5dfLzb9of7uuXMXYC0fJIeZTw+shS32o2cuQ9jF7YSDuL/oQ=="; export const PI_BIN = join("node_modules", PI_PACKAGE, "dist", "bundle", "cli.js"); export const SEAL_FLAGS = Object.freeze(["--no-extensions", "--no-prompt-templates", "--no-themes"]); export const ENGINE_OPTIONS = Object.freeze(["--model", "--provider", "--thinking"]); export const ENGINE_PIN_MISMATCH = "engine-pin-mismatch"; export const UNSEALED_ENGINE = "unsealed-engine"; function lockEntry(path) { let lock; try { lock = JSON.parse(readFileSync(path, "utf8")); } catch { return null; } const entry = lock?.packages?.[`node_modules/${PI_PACKAGE}`]; return entry && typeof entry === "object" ? entry : null; } // `root` holds package-lock.json and node_modules/.package-lock.json. export function checkEnginePin(root) { for (const path of [join(root, "package-lock.json"), join(root, "node_modules", ".package-lock.json")]) { const entry = lockEntry(path); if (!entry || entry.version !== PI_VERSION || entry.integrity !== PI_INTEGRITY) { throw new ControlRefusal(ENGINE_PIN_MISMATCH, `${path} does not pin ${PI_PACKAGE} ${PI_VERSION} with the pinned integrity`); } } return { version: PI_VERSION, pin: PI_INTEGRITY }; } export function buildPiArgs({ sessionFile, extraArgs = [] }) { return ["--mode", "rpc", ...SEAL_FLAGS, "--session", sessionFile, ...extraArgs]; } // Refuses any argv that is not `--mode rpc`, the three --no-* flags and // `--session `, in that order, followed by ENGINE_OPTIONS // pairs. That covers --extension in either spelling, a second --mode or // --session, session and output flags (--no-session, --fork, --export, ...) // and stray prompt words. export function checkSeal(args) { if (!Array.isArray(args) || args.some((a) => typeof a !== "string")) throw new ControlRefusal(UNSEALED_ENGINE, "launch argv is not a list of strings"); const extension = args.find((a) => a === "-e" || a === "--extension" || a.startsWith("--extension=")); if (extension !== undefined) throw new ControlRefusal(UNSEALED_ENGINE, `launch argv carries ${extension}`); for (const flag of SEAL_FLAGS) { if (!args.includes(flag)) throw new ControlRefusal(UNSEALED_ENGINE, `launch argv lacks ${flag}`); } const prefix = ["--mode", "rpc", ...SEAL_FLAGS, "--session"]; if (prefix.some((a, i) => args[i] !== a)) throw new ControlRefusal(UNSEALED_ENGINE, `launch argv does not start with ${prefix.join(" ")}`); const file = args[prefix.length]; if (typeof file !== "string" || !isAbsolute(file)) throw new ControlRefusal(UNSEALED_ENGINE, "the --session value is not an absolute path"); const seen = new Set(); for (let i = prefix.length + 1; i < args.length; i += 2) { const flag = args[i], value = args[i + 1]; if (!ENGINE_OPTIONS.includes(flag)) throw new ControlRefusal(UNSEALED_ENGINE, `launch argv carries ${flag}, which is not one of ${ENGINE_OPTIONS.join(", ")}`); if (seen.has(flag)) throw new ControlRefusal(UNSEALED_ENGINE, `launch argv repeats ${flag}`); if (typeof value !== "string" || !value || value.startsWith("-") || value.startsWith("@")) throw new ControlRefusal(UNSEALED_ENGINE, `${flag} needs one plain value`); seen.add(flag); } return true; } export function argvDigest(command, args) { return createHash("sha256").update(JSON.stringify([command, ...args])).digest("hex"); }