Files
PaperClipAI/packages/paperclip-runner/scripts/controlled-evals/hosted.test.mjs
T
DottaandPaperclip cff331f6ca fix(evals): retain local controller cost origin (#17990)
## Thinking Path

> - Paperclip manages AI agents and their work.
> - Its eval journals retain model costs and operating reservations
under one cap.
> - The operating policy records the controller's execution origin.
> - The schema currently accepts only GitHub-hosted and AWS Fleet
origins.
> - A local controller must therefore use an inaccurate label or fail
admission.
> - This PR adds the local origin and keeps the hosted-only schedule
gate.

## Linked Issues or Issue Description

Refs #17986. No duplicate open PR or issue was found in the
operating-overhead/local search.

**What existing behavior does this improve?**

The Product E2E operating allowance and its retained journal policy.

**Current behavior**

At master `40c1ab36cd`, an unscheduled local controller cannot set
`runner: "local"`. The generic allowance validator rejects it. Local
qualification can use the same accounting controls but cannot record its
origin correctly.

**Proposed behavior**

Accept the local origin in the generic allowance. Keep all component,
price, cap, expiry, and receipt rules. The scheduled validator still
requires `github-hosted` and rejects a local allowance.

**Reason and benefit**

Local qualification can retain an accurate execution origin without
claiming that its reservations are billed GitHub charges.

## What Changed

- Add `local` to the operating allowance's runner type and validator.
- Exercise local journal creation, model admission, retained overhead,
restart, and reconciliation in the existing boundary test.
- Check that a local allowance fails Product scheduled admission.
- Pin new hosted Runner controller admission to `github-hosted`.
Preserve original labels when reading historical manifests; those reads
cannot authorize a new worker.
- Exercise the hosted-origin rejection and historical/local reads in
both source and copied portable runtimes.
- Document the local label and its accounting limits.

## Verification

- Before the fix: the new local persistence case and schedule-boundary
assertion failed; eight tests passed.
- Focused ledger, overhead, operations, and scheduled finalization
tests: 71 passed across five files.
- Controlled Runner source and portable tests: 86 passed. The new
hosted-origin regression failed before the fix.
- Full Product support suite at the first commit: 4,524 passed, three
skipped, and one retained embedded PostgreSQL bootstrap failure. The
same database failure was previously reproduced on a clean baseline.
- Separate Node test phase on the host: 137 passed.
- Actual Linux controller: database lifecycle tests pass. After
installing its missing `zip` utility, the full Product suite has 4,527
passes and one Copilot parent-version coverage failure. That same
failure reproduces on clean master `40c1ab36cd` with the same Linux
filesystem and dependencies (14 passes, one failure). Copilot files are
unchanged. The initial seven ZIP-related failures and the watcher
failure remain retained.
- The final fresh Linux semantic check passes: `GOMEMLIMIT=2GiB GOGC=50
GOMAXPROCS=1 node node_modules/typescript/bin/tsc --singleThreaded -p
tests/runner-e2e/tsconfig.json`. The parallel compiler OOM is retained.
No semantic checks were omitted.
- Both fresh reviews are 5/5, with the addressed thread resolved. Hosted
Build first failed during unchanged Pi setup prefetch with an operation
timeout. One job rerun recovered; the original failure remains retained.
All 58 latest-head checks are now acceptable.
- No paid model requests, sandbox creation, schedule changes, or
historical journal edits.

## Risks

- The caller supplies this origin. It must describe the actual
controller.
- `local` does not prove zero compute cost, provide a default allowance,
or release a hold.
- Both maintained hosted admission paths require the GitHub-hosted
controller origin. A previously accepted hosted policy with an AWS/local
label now fails new admission. Historical reads preserve the original
evidence.

## Model Used

- OpenAI GPT-6 through Codex, with tool use and code execution. The
exact serving model ID and context window are not exposed in this
session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-28 02:05:36 -05:00

190 lines
14 KiB
JavaScript

import assert from 'node:assert/strict';
import { test } from 'node:test';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { fixture, success } from './controller-fixture.mjs';
import { canonical, sha256, validateManifest } from './manifest.mjs';
const runtime = process.env.PAPERCLIP_CONTROLLER_TEST_RUNTIME ? pathToFileURL(resolve(process.env.PAPERCLIP_CONTROLLER_TEST_RUNTIME) + '/') : import.meta.url;
const { hostedPaths, hostedPolicyConfiguration, prepareHostedManifest, verifyHostedCell, hostedAdmissionDiagnostic } = await import(new URL('./hosted.mjs', runtime));
const { packControlledAttempt, summarizeControlledManifest } = await import(new URL('./archive.mjs', runtime));
async function campaign(t) {
const f = await fixture(t, { count: 2, observability: true });
const runId = String(Date.now() * 1000 + Math.floor(Math.random() * 1000)), paths = hostedPaths(runId);
t.after(() => rm(paths.root, { recursive: true, force: true }));
await mkdir(join(paths.runner, 'portable/dist/cli'), { recursive: true });
const artifacts = {};
for (const [name, relative] of [['runnerCli', 'portable/dist/cli/eval-session.js'], ['runnerPackage', 'paperclip-runner.tgz'], ['runnerd', 'paperclip-runnerd']]) {
artifacts[name] = { ...f.manifest[name], path: join(paths.runner, relative) };
await cp(f.manifest[name].path, artifacts[name].path);
}
const cells = f.requests.map(({ request }, index) => ({ cellId: `execution-${index}`, provider: request.provider, driver: 'codex_app_server', model: request.model }));
const catalog = { schema: 'paperclip.runner-protocol-eval.catalog/v1', campaignId: `gha-${runId}-1`,
source: { paperclipSha: f.manifest.sourceRevision, evalsSha: f.manifest.definitionsRevision },
cells, matrices: cells.map(cell => ({ include: [cell] })) };
const catalogSha256 = sha256(canonical(catalog));
for (const item of f.requests) { item.request.runnerd = { ...artifacts.runnerd, sha256: `sha256:${artifacts.runnerd.sha256}` }; item.request.acpxAgent = null; item.request.driver = 'codex_app_server'; await writeFile(item.requestPath, JSON.stringify(item.request)); }
const prepared = { schema: 'paperclip.runner-eval.prepared-requests/v1', campaignId: catalog.campaignId, githubRunId: runId, githubRunAttempt: 1,
sourceRevision: f.manifest.sourceRevision, definitionsRevision: f.manifest.definitionsRevision, catalogSha256, runnerd: f.requests[0].request.runnerd,
cells: cells.map((cell, index) => ({ cellId: cell.cellId, runId: `${catalog.campaignId}-${cell.cellId}`, request: f.requests[index].request })) };
for (const component of f.manifest.overhead.components) component.provisionalBound.evidenceSha256 = sha256(component.component);
const { provisionalBound, ...observability } = f.manifest.observability;
const { executionId, attemptId, requestSha256, ...allocation } = f.manifest.attempts[0];
const policy = { schema: 'paperclip.runner-eval.hosted-policy/v1', controllerLockSha256: 'e'.repeat(64), maxUsd: f.manifest.maxUsd, maxCells: 2, durationMs: 60000,
pricingVerifiedAt: f.manifest.pricingVerifiedAt, pricingExpiresAt: f.manifest.pricingExpiresAt,
route: 'direct', models: f.manifest.models, overhead: f.manifest.overhead, observability, allocations: [allocation] };
policy.capacityBound = { version: 1, scope: 'full_lifecycle', evidenceSha256: f.manifest.overhead.components.find(component => component.component === 'observability').provisionalBound.evidenceSha256, configurationSha256: sha256(canonical(hostedPolicyConfiguration(policy))) };
const environment = { ...f.environment, GITHUB_ACTIONS: 'true', GITHUB_REPOSITORY: 'paperclipai/paperclip', GITHUB_RUN_ID: runId,
GITHUB_RUN_ATTEMPT: '1', GITHUB_SHA: f.manifest.sourceRevision, PAPERCLIP_PROTOCOL_EVAL_SOURCE_SHA: prepared.sourceRevision, PAPERCLIP_PROTOCOL_EVALS_SHA: prepared.definitionsRevision };
const input = { catalog, catalogSha256, prepared, policy, artifacts, environment };
const build = () => prepareHostedManifest(input);
return { f, input, paths, environment, build };
}
test('one prepared manifest binds disjoint workers and retains partial or entirely missing campaigns', async t => {
const c = await campaign(t), result = c.build();
Object.assign(c.f.manifest, result.manifest); await c.f.save();
const empty = await summarizeControlledManifest(result.manifest, []);
assert.equal(empty.incompleteAttempts, 2); assert.equal(empty.heldUsd, '0.024000000');
assert.equal(empty.costStatus, 'incomplete');
Object.assign(c.f.environment, c.environment, { PAPERCLIP_RUNNER_EVAL_EXECUTION_ID: 'execution-0' });
assert.equal((await c.f.run(0, () => success())).code, 0);
const bundle = await packControlledAttempt({ manifest: result.manifest, attemptId: 'attempt-0', environment: c.f.environment });
const partial = await summarizeControlledManifest(result.manifest, [bundle, bundle]);
assert.equal(partial.incompleteAttempts, 1); assert.equal(partial.knownUsd, '0.000136000');
assert.equal(partial.heldUsd, '0.014000000'); assert.equal(partial.operatingHeldUsd, '0.004000000');
Object.assign(c.f.environment, { PAPERCLIP_RUNNER_EVAL_EXECUTION_ID: 'execution-1' });
assert.equal((await c.f.run(1, () => success())).code, 0);
const second = await packControlledAttempt({ manifest: result.manifest, attemptId: 'attempt-1', environment: c.f.environment });
const complete = await summarizeControlledManifest(result.manifest, [bundle, second]);
assert.equal(complete.incompleteAttempts, 0); assert.equal(complete.knownUsd, '0.000272000');
assert.equal(complete.heldUsd, '0.004000000'); assert.equal(complete.costStatus, 'provisional');
await assert.rejects(summarizeControlledManifest({ ...result.manifest, maxUsd: '2' }, [bundle]), /retained campaign manifest/);
});
test('hosted preparation rejects selection drift, unsupported cells and unreviewed or insufficient budgets', async t => {
const c = await campaign(t);
const changes = [
input => { input.prepared.definitionsRevision = 'd'.repeat(40); },
input => { input.prepared.catalogSha256 = '0'.repeat(64); },
input => { input.catalog.matrices[1].include = input.catalog.matrices[0].include; },
input => { input.prepared.cells[1] = input.prepared.cells[0]; },
input => { input.prepared.cells[0].request.provider = 'opencode'; },
input => { input.prepared.cells[0].request.driver = 'unsupported'; },
input => { input.catalog.cells[0].driver = 'unsupported'; input.catalog.matrices[0].include[0].driver = 'unsupported'; },
input => { input.prepared.cells[0].request.runnerd.sha256 = 'sha256:' + '0'.repeat(64); },
input => { input.policy.maxUsd = '0.01'; },
input => { input.environment.GITHUB_RUN_ATTEMPT = '2'; },
input => { input.environment.GITHUB_SHA = 'wrong'; },
input => { input.environment.GITHUB_SHA = 'c'.repeat(40); },
input => { delete input.policy.observability; },
input => { delete input.policy.controllerLockSha256; },
input => { input.policy.controllerLockSha256 = 'f'.repeat(64); },
];
for (const change of changes) {
const input = structuredClone(c.input); change(input);
assert.throws(() => prepareHostedManifest(input));
}
const mixed = structuredClone(c.input);
mixed.policy.capacityBound.evidenceSha256 = mixed.policy.overhead.components.find(component => component.component !== 'observability').provisionalBound.evidenceSha256;
assert.throws(() => prepareHostedManifest(mixed), /Observability capacity requires its operating bound evidence/);
const input = structuredClone(c.input); input.policy.maxUsd = '0.01';
input.policy.capacityBound.configurationSha256 = sha256(canonical(hostedPolicyConfiguration(input.policy)));
assert.throws(() => prepareHostedManifest(input), /budget/);
});
test('hosted admission rejects local and fleet origins while preserving retained manifests', async t => {
const c = await campaign(t);
c.input.policy.overhead.accountingReadiness = 'model_requests';
for (const component of c.input.policy.overhead.components) delete component.provisionalBound;
delete c.input.policy.capacityBound;
const original = c.build().manifest;
for (const runner of ['local', 'aws-fleet']) {
const input = structuredClone(c.input); input.policy.overhead.runner = runner;
assert.throws(() => prepareHostedManifest(input), /GitHub-hosted operating origin/);
const retained = structuredClone(original); retained.overhead.runner = runner;
assert.equal(validateManifest(retained, Date.now(), { admission: false }).manifest.overhead.runner, runner);
await writeFile(c.paths.manifest, canonical(retained));
await assert.rejects(verifyHostedCell({ manifestPath: c.paths.manifest, manifestSha256: sha256(canonical(retained)),
cellId: 'execution-0', environment: { ...c.environment, PAPERCLIP_RUNNER_EVAL_EXECUTION_ID: 'execution-0' } }), /GitHub-hosted operating origin/);
}
const local = structuredClone(original); delete local.hosted; local.overhead.runner = 'local';
assert.equal(validateManifest(local).manifest.overhead.runner, 'local');
});
test('worker preflight verifies identical manifest bytes and current cell/run before credentials', async t => {
const c = await campaign(t), { manifest, manifestSha256 } = c.build();
await writeFile(c.paths.manifest, canonical(manifest));
const input = { manifestPath: c.paths.manifest, manifestSha256, cellId: 'execution-0',
environment: { ...c.environment, PAPERCLIP_RUNNER_EVAL_EXECUTION_ID: 'execution-0' } };
assert.equal((await verifyHostedCell(input)).attempt.attemptId, 'attempt-0');
await assert.rejects(verifyHostedCell({ ...input, cellId: 'another-cell' }), /outside/);
await assert.rejects(verifyHostedCell({ ...input, environment: { ...input.environment, GITHUB_RUN_ATTEMPT: '2' } }), /first-attempt/);
await writeFile(c.paths.manifest, canonical(manifest) + '\n');
await assert.rejects(verifyHostedCell(input), /bytes changed/);
});
test('hosted CLI reports safe admission reasons without echoing private paths or malformed input', async () => {
assert.equal(hostedAdmissionDiagnostic(new Error('Reviewed cost bound configuration changed')), 'Reviewed cost bound configuration changed');
assert.equal(hostedAdmissionDiagnostic(new SyntaxError('SENSITIVE_SENTINEL')), 'A hosted input is not valid JSON');
assert.doesNotMatch(hostedAdmissionDiagnostic(new Error('private SENSITIVE_SENTINEL')), /SENSITIVE_SENTINEL/);
const result = await promisify(execFile)(process.execPath, [fileURLToPath(new URL('./hosted.mjs', runtime)), 'verify', '/nonexistent/SENSITIVE_SENTINEL', 'a'.repeat(64), 'cell'], {
env: { PATH: process.env.PATH, OPENAI_API_KEY: 'SENSITIVE_SENTINEL' }, timeout: 10000,
}).then(value => value, error => error);
assert.equal(result.code, 2);
assert.match(result.stderr, /A required hosted input file is missing/);
assert.doesNotMatch(result.stderr + result.stdout, /SENSITIVE_SENTINEL/);
});
test('model-readiness policy retains operating holds without requiring vendor cost proofs', async t => {
const c = await campaign(t);
c.input.policy.overhead.accountingReadiness = 'model_requests';
for (const component of c.input.policy.overhead.components) delete component.provisionalBound;
delete c.input.policy.capacityBound;
const result = c.build();
assert.equal(result.manifest.overhead.accountingReadiness, 'model_requests');
assert.equal(result.manifest.observability.provisionalBound, undefined);
Object.assign(c.f.manifest, result.manifest); await c.f.save();
const missing = await summarizeControlledManifest(result.manifest, []);
assert.equal(missing.incompleteAttempts, 2);
assert.equal(missing.operatingHeldUsd, '0.004000000');
Object.assign(c.f.environment, c.environment, { PAPERCLIP_RUNNER_EVAL_EXECUTION_ID: 'execution-0' });
assert.equal((await c.f.run(0, () => success())).code, 0);
const first = await packControlledAttempt({ manifest: result.manifest, attemptId: 'attempt-0', environment: c.f.environment });
Object.assign(c.f.environment, { PAPERCLIP_RUNNER_EVAL_EXECUTION_ID: 'execution-1' });
assert.equal((await c.f.run(1, () => success())).code, 0);
const second = await packControlledAttempt({ manifest: result.manifest, attemptId: 'attempt-1', environment: c.f.environment });
const complete = await summarizeControlledManifest(result.manifest, [first, second]);
assert.equal(complete.incompleteAttempts, 0);
assert.equal(complete.operatingHeldUsd, '0.004000000');
assert.equal(complete.accountingReadiness, 'model_requests');
assert.equal(complete.operatingReconciliation, 'pending');
delete c.input.policy.overhead.accountingReadiness;
assert.throws(() => c.build(), /cost bound|operating bound/);
});
test('model readiness still validates supplied operating and capacity proofs', async t => {
const c = await campaign(t), policy = c.input.policy;
policy.overhead.accountingReadiness = 'model_requests';
for (const component of policy.overhead.components) {
component.provisionalBound.configurationSha256 = sha256(canonical({ accountingReadiness: 'model_requests',
reference: policy.overhead.reference, runner: policy.overhead.runner, verifiedAt: policy.overhead.verifiedAt,
expiresAt: policy.overhead.expiresAt, component: { component: component.component, maxUsd: component.maxUsd, reference: component.reference } }));
}
policy.capacityBound.configurationSha256 = sha256(canonical(hostedPolicyConfiguration(policy)));
assert.equal(c.build().manifest.overhead.accountingReadiness, 'model_requests');
const mismatch = structuredClone(c.input); mismatch.policy.capacityBound.evidenceSha256 = '0'.repeat(64);
assert.throws(() => prepareHostedManifest(mismatch), /Observability capacity requires its operating bound evidence/);
const stale = structuredClone(c.input); stale.policy.capacityBound.configurationSha256 = '0'.repeat(64);
assert.throws(() => prepareHostedManifest(stale), /Reviewed cost bound configuration changed/);
const absent = structuredClone(c.input);
for (const component of absent.policy.overhead.components) delete component.provisionalBound;
absent.policy.capacityBound.configurationSha256 = sha256(canonical(hostedPolicyConfiguration(absent.policy)));
assert.throws(() => prepareHostedManifest(absent), /Observability capacity requires its operating bound evidence/);
});