Dewey's round 3 candidate, manifest
agents/dewey/work/queue-40/candidate-manifest-r3.sha256 (d0aa0ded,
27 files, checked OK in the canonical tree).
- WebUI inbox, tasks, agents and trail views, read-only over /api/bus.
The README says the bus proof ends at the Console process.
- CHAT-03 seal: the engine command is fixed, the engine environment is
explicit, SEAL_FLAGS has --no-approve, escalating is cleared on throw.
- Terminal input typed after Ctrl-T or Ctrl-O is held. Only the run whose
own parse set held drains it (T1), and #run catches errors per action.
- DEFERRED keeps N2 and moves F2 to done, citing T1.
Reviews: Filbert approve (comment 27011, rev 260), Darkwing approve
(27013, rev 264). Landing gate on 8cad7722 plus the candidate: webui 22,
conversation 161, control-board 124, every scripts/test-*.sh green,
test-task 98/0. Mutant Mr survives; its flows test is the first
follow-up row.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
115 lines
6.4 KiB
JavaScript
115 lines
6.4 KiB
JavaScript
// 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, --no-themes and --no-approve, and
|
||
// never an --extension argument (cli/args.js; usage.md 224 and 233–236).
|
||
// --no-approve sets the project trust override to false, so a project's
|
||
// .pi/settings.json, SYSTEM.md, APPEND_SYSTEM.md and skills don't load even
|
||
// when trust.json under the agent dir trusts it (main.js 574–581;
|
||
// usage.md 126; Filbert F2 on #1522). 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", "--no-approve"]);
|
||
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 four --no-* flags and
|
||
// `--session <absolute path>`, 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;
|
||
}
|
||
|
||
// The engine's environment (I3, Darkwing F3 on #1507): built from names,
|
||
// never inherited whole. ENGINE_ENV names what Pi needs to run; a launch may
|
||
// add provider credentials by name (`engine.envKeys`), and nothing else, so
|
||
// NODE_OPTIONS, LD_PRELOAD and the PI_PACKAGE_DIR family can't load code
|
||
// around the seal. Values come from the controller's own environment; an
|
||
// unset name is left out, not set empty.
|
||
export const ENGINE_ENV = Object.freeze(["PATH", "HOME", "USER", "LOGNAME", "SHELL", "LANG", "LC_ALL", "LC_CTYPE", "TZ", "TERM", "TMPDIR", "PI_CODING_AGENT_DIR", "PI_OFFLINE", "PI_SKIP_VERSION_CHECK", "PI_TELEMETRY"]);
|
||
export const CREDENTIAL_NAME = /^[A-Z][A-Z0-9_]{0,62}_(API_KEY|TOKEN)$/;
|
||
|
||
export function engineEnv(envKeys = [], source = process.env) {
|
||
if (!Array.isArray(envKeys)) throw new ControlRefusal(UNSEALED_ENGINE, "engine.envKeys is not a list");
|
||
const bad = envKeys.find((k) => typeof k !== "string" || !CREDENTIAL_NAME.test(k));
|
||
if (bad !== undefined) throw new ControlRefusal(UNSEALED_ENGINE, `engine.envKeys names ${String(bad).slice(0, 80)}, which is not a provider credential (*_API_KEY or *_TOKEN)`);
|
||
const env = {};
|
||
for (const k of [...ENGINE_ENV, ...envKeys]) if (typeof source[k] === "string") env[k] = source[k];
|
||
return env;
|
||
}
|
||
|
||
export function argvDigest(command, args) {
|
||
return createHash("sha256").update(JSON.stringify([command, ...args])).digest("hex");
|
||
}
|