Files
PaperClipAI/packages/paperclip-runner/scripts/controlled-evals/build-runtime.mjs
T
DottaandPaperclip 8c702d59db feat(evals): package a portable Runner controller runtime (#17506)
Package a verified portable Runner eval controller with complete bounded file receipts, copied dependency closure, private evidence replay and symlink-safe CLI entrypoints. Preserve fail-closed cost holds, native trace receipts and all parent security repairs.

Verified head: 1c892ff464.
Validation: 72 synthetic source/portable tests pass after current-master sync. Full current-head CI passed (40386180906); Greptile reviewed this exact head at5/5, with zero unresolved findings.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-26 14:32:46 -05:00

96 lines
6.5 KiB
JavaScript

import { isEntrypoint } from './entrypoint.mjs';
import { createHash } from 'node:crypto';
import { createRequire } from 'node:module';
import { cp, mkdir, readFile, readdir, realpath, writeFile } from 'node:fs/promises';
import { basename, dirname, join, relative, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { encodeRuntimeReceipt, RUNTIME_MAX_BYTES, RUNTIME_MAX_FILES } from './verify-runtime.mjs';
const source = dirname(fileURLToPath(import.meta.url));
const repository = resolve(source, '../../../..');
const require = createRequire(join(repository, 'server/package.json'));
const sha256 = bytes => createHash('sha256').update(bytes).digest('hex');
const scripts = ['entrypoint.mjs', 'evidence-files.mjs', 'archive.mjs', 'manifest.mjs', 'observability.mjs', 'report.mjs', 'run.mjs', 'verify-runtime.mjs'];
export async function buildControllerRuntime({ outputDirectory, sourceRevision }) {
if (!/^[a-f0-9]{40}$/.test(sourceRevision)) throw new Error('Controller runtime requires an exact source revision');
const output = resolve(outputDirectory);
await mkdir(output, { mode: 0o700 });
const controller = join(output, 'controlled-evals');
await mkdir(controller, { mode: 0o700 });
for (const file of scripts) await cp(join(source, file), join(controller, file), { errorOnExist: true, force: false });
const runtime = join(output, 'tests/runner-e2e/shared-runtime.mjs');
await mkdir(dirname(runtime), { recursive: true, mode: 0o700 });
const { build } = await import(require.resolve('esbuild'));
const result = await build({ entryPoints: [join(source, 'shared-cost-runtime.ts')], outfile: runtime, bundle: true,
platform: 'node', format: 'esm', target: 'node24', metafile: true, logLevel: 'silent',
banner: { js: "import { createRequire as __paperclipCreateRequire } from 'node:module'; const require = __paperclipCreateRequire(import.meta.url);" } });
if (result.warnings.length) throw new Error('Controller runtime bundle emitted unresolved build warnings');
const { isBuiltin } = await import('node:module');
if (Object.values(result.metafile.outputs).some(item => item.imports.some(item => !isBuiltin(item.path)))) throw new Error('Controller runtime has unbundled imports');
async function packageManifest(name, parentRequire) {
try { return await realpath(parentRequire.resolve(`${name}/package.json`)); }
catch (error) { if (error.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') throw error; }
let directory = dirname(await realpath(parentRequire.resolve(name)));
while (true) {
const candidate = join(directory, 'package.json');
try {
if (JSON.parse(await readFile(candidate, 'utf8')).name === name) return candidate;
} catch (error) { if (error.code !== 'ENOENT') throw error; }
const parent = dirname(directory);
if (parent === directory) throw new Error('Controller dependency package metadata is missing');
directory = parent;
}
}
let packages = 0;
const packageInputs = {};
async function copyPackage(name, parentRequire, destination, ancestors = new Set()) {
if (++packages > 128 || ancestors.size > 16) throw new Error('Controller dependency closure exceeds its limit');
const manifest = await packageManifest(name, parentRequire);
if (ancestors.has(manifest)) throw new Error('Controller dependency closure contains an unsupported cycle');
const bytes = await readFile(manifest), metadata = JSON.parse(bytes.toString('utf8'));
packageInputs[`${metadata.name}@${metadata.version}`] = sha256(bytes);
const target = join(destination, 'node_modules', name);
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
await cp(dirname(manifest), target, { recursive: true, dereference: true, errorOnExist: true, force: false,
filter: path => basename(path) !== 'node_modules' });
for (const dependency of Object.keys(metadata.dependencies ?? {}).sort()) {
await copyPackage(dependency, createRequire(manifest), target, new Set([...ancestors, manifest]));
}
}
const server = join(output, 'server'); await mkdir(server, { mode: 0o700 });
await writeFile(join(server, 'package.json'), '{"private":true}\n', { flag: 'wx', mode: 0o600 });
await copyPackage('protobufjs', require, server);
await writeFile(join(controller, 'runtime.mjs'), "export * as costRuntime from '../tests/runner-e2e/shared-runtime.mjs';\n", { flag: 'wx', mode: 0o600 });
const inputs = {};
for (const path of Object.keys(result.metafile.inputs)) {
const absolute = resolve(path);
inputs[relative(repository, absolute).split('\\').join('/')] = sha256(await readFile(absolute));
}
for (const file of [...scripts, 'build-runtime.mjs', 'runtime.mjs']) inputs[relative(repository, join(source, file))] = sha256(await readFile(join(source, file)));
const files = {}; let payloadBytes = 0;
async function inventory(directory) {
for (const entry of await readdir(directory, { withFileTypes: true })) {
const path = join(directory, entry.name);
if (entry.isDirectory()) await inventory(path);
else if (entry.isFile()) {
const bytes = await readFile(path); payloadBytes += bytes.length;
if (payloadBytes > RUNTIME_MAX_BYTES || Object.keys(files).length >= RUNTIME_MAX_FILES - 1) throw new Error('Controller runtime exceeds its artifact allowance');
files[relative(output, path).split('\\').join('/')] = { sha256: sha256(bytes), sizeBytes: bytes.length };
} else throw new Error('Controller runtime contains a nonregular file');
}
}
await inventory(output);
const receipt = { schema: 'paperclip.runner-eval.controller-runtime/v1', sourceRevision, nodeMajor: 24, inputs, packageInputs, payloadBytes, files };
const receiptBytes = encodeRuntimeReceipt(receipt);
if (payloadBytes + Buffer.byteLength(receiptBytes) > RUNTIME_MAX_BYTES) throw new Error('Controller runtime receipt exceeds its artifact allowance');
await writeFile(join(output, 'runtime.json'), receiptBytes, { flag: 'wx', mode: 0o600 });
return { ...receipt, totalBytes: payloadBytes + Buffer.byteLength(receiptBytes), receiptSha256: sha256(receiptBytes) };
}
if (await isEntrypoint(import.meta.url)) {
if (process.argv.length !== 4) throw new Error('Usage: build-runtime.mjs <new-output-directory> <source-revision>');
const receipt = await buildControllerRuntime({ outputDirectory: process.argv[2], sourceRevision: process.argv[3] });
process.stdout.write(JSON.stringify({ sourceRevision: receipt.sourceRevision, totalBytes: receipt.totalBytes, receiptSha256: receipt.receiptSha256, files: Object.keys(receipt.files).length + 1 }) + '\n');
}