mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-30 05:16:50 +01:00
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>
96 lines
6.5 KiB
JavaScript
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');
|
|
}
|