mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 04:02:18 +01:00
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:
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({
|
||||
|
||||
Reference in new issue
Block a user