fix(evals): align portable receipt bounds before publication

Also cover createdFromIssueId in the list and count UUID rejection tests from the master merge.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-24 16:03:27 -07:00
1 parent 20511856d4
commit 4e719ac4c4
5 files changed
+18 -5

No files matched your search

@@ -348,7 +348,8 @@ lifecycle scripts. `runtime.json` records the caller-supplied source revision,
source input hashes, installed package manifest hashes, and every output file's
size and SHA-256. The caller must build from the intended trusted checkout; the
revision label alone is not source attestation. The artifact is limited to
32 MiB and 4,096 files. It contains no campaign credentials or allowance.
32 MiB and 4,096 files, including a receipt limited to 2 MiB by both the builder
and verifier. It contains no campaign credentials or allowance.
Save the printed `receiptSha256` outside the artifact, such as a trusted build
job output. Before a worker loads any copied code, invoke the verifier from its
@@ -4,7 +4,7 @@ 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 { RUNTIME_MAX_BYTES, RUNTIME_MAX_FILES } from './verify-runtime.mjs';
import { encodeRuntimeReceipt, RUNTIME_MAX_BYTES, RUNTIME_MAX_FILES } from './verify-runtime.mjs';
const source = dirname(fileURLToPath(import.meta.url));
const repository = resolve(source, '../../../..');
@@ -82,7 +82,7 @@ export async function buildControllerRuntime({ outputDirectory, sourceRevision }
}
await inventory(output);
const receipt = { schema: 'paperclip.runner-eval.controller-runtime/v1', sourceRevision, nodeMajor: 24, inputs, packageInputs, payloadBytes, files };
const receiptBytes = JSON.stringify(receipt, null, 2) + '\n';
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) };
@@ -7,7 +7,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
import { promisify } from 'node:util';
import { test } from 'node:test';
import { buildControllerRuntime } from './build-runtime.mjs';
import { verifyControllerRuntime } from './verify-runtime.mjs';
import { encodeRuntimeReceipt, RUNTIME_MAX_RECEIPT_BYTES, verifyControllerRuntime } from './verify-runtime.mjs';
const exec = promisify(execFile);
const source = dirname(fileURLToPath(import.meta.url));
@@ -56,6 +56,10 @@ test('copied controller runs without source dependencies, retains native evidenc
await rename(built, directory);
const input = { directory, sourceRevision, receiptSha256: receipt.receiptSha256 };
assert.equal((await verifyControllerRuntime(input)).totalBytes, receipt.totalBytes);
const rawReceipt = JSON.parse(await readFile(join(directory, 'runtime.json'), 'utf8'));
assert(encodeRuntimeReceipt(rawReceipt).length < RUNTIME_MAX_RECEIPT_BYTES);
const manyInputs = Object.fromEntries(Array.from({ length: 25_000 }, (_, index) => [`src/part-${index}.ts`, 'a'.repeat(64)]));
assert.throws(() => encodeRuntimeReceipt({ ...rawReceipt, inputs: manyInputs }), /receipt exceeds its byte allowance/);
const environment = { PATH: process.env.PATH, ...(process.env.LANG ? { LANG: process.env.LANG } : {}) };
const probe = join(root, 'probe.mjs'); await writeFile(probe, isolatedProbe);
const isolated = await exec(process.execPath, [probe, directory], { cwd: root, env: environment, timeout: 20_000 });
@@ -6,6 +6,12 @@ import { join, resolve } from 'node:path';
export const RUNTIME_MAX_BYTES = 32 * 1_048_576;
export const RUNTIME_MAX_FILES = 4096;
export const RUNTIME_MAX_RECEIPT_BYTES = 2 * 1_048_576;
export function encodeRuntimeReceipt(receipt) {
const bytes = Buffer.from(JSON.stringify(receipt, null, 2) + '\n');
if (bytes.length > RUNTIME_MAX_RECEIPT_BYTES) throw new Error('Controller runtime receipt exceeds its byte allowance');
return bytes;
}
const sha256 = bytes => createHash('sha256').update(bytes).digest('hex');
async function readRegular(path, limit) {
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
@@ -28,7 +34,7 @@ export async function verifyControllerRuntime({ directory, sourceRevision, recei
if (!/^[a-f0-9]{40}$/.test(sourceRevision ?? '') || !/^[a-f0-9]{64}$/.test(receiptSha256 ?? '')) throw new Error('Runtime verification requires trusted revision and receipt digest');
const root = resolve(directory);
if (!(await lstat(root)).isDirectory()) throw new Error('Runtime root must be a real directory');
const receiptBytes = await readRegular(join(root, 'runtime.json'), 2 * 1_048_576);
const receiptBytes = await readRegular(join(root, 'runtime.json'), RUNTIME_MAX_RECEIPT_BYTES);
if (sha256(receiptBytes) !== receiptSha256) throw new Error('Runtime receipt digest mismatch');
const receipt = JSON.parse(receiptBytes.toString('utf8'));
if (receipt.schema !== 'paperclip.runner-eval.controller-runtime/v1' || receipt.sourceRevision !== sourceRevision
@@ -800,6 +800,7 @@ describeEmbeddedPostgres("issue list routes assigneeAgentId filter", () => {
"descendantOf",
"labelId",
"participantAgentId",
"createdFromIssueId",
])("returns 422 instead of a DB error for malformed %s filters", async (param) => {
const companyId = randomUUID();
await db.insert(companies).values({
@@ -828,6 +829,7 @@ describeEmbeddedPostgres("issue list routes assigneeAgentId filter", () => {
"descendantOf",
"labelId",
"participantAgentId",
"createdFromIssueId",
])("returns 422 from issues/count for malformed %s filters", async (param) => {
const companyId = randomUUID();
await db.insert(companies).values({