mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-08 22:13:59 +01:00
## Thinking Path > - Paperclip lets people manage AI agent work through a governed control plane. > - Runner protocol qualification uses real providers and records selected cases in TestOps. > - A manual registration run starts an Allure watch session before it validates the case selection. > - Watch creates the job run, but it does not fill the empty job-run input in the child environment. > - The selection helper therefore stops before it can validate the new job run. > - A configured AWS concurrency ceiling can also reject a valid smaller hosted-runner request. > - Hosted tool-cache permissions can also violate Runner qualified-executable admission before a provider starts. > - This pull request captures official session identities, keeps both concurrency ceilings, and stages the same Node bytes in an owned directory. ## Linked Issues or Issue Description Refs #18985. **What happened?** The [first registration attempt](https://github.com/paperclipai/paperclip/actions/runs/41877603952) created an Allure job run, then stopped with `runner_testops_jobrun_missing`. The child inherited an empty `ALLURE_JOB_RUN_ID`. The helper made no job-run API read and executed no provider case. The current configured concurrency ceiling is 100. The controlled hosted cap is 57. The original catalog guard rejects 100 before it applies a valid requested value of 2. The [first service-selected attempt](https://github.com/paperclipai/paperclip/actions/runs/41884365232) reproduced this second pre-provider failure. Its selected case was correctly reported as broken. No provider cell ran. A [second service-selected attempt](https://github.com/paperclipai/paperclip/actions/runs/41887671585) passed selection, lock and cost preflight. Runner rejected a qualified ACPX launch artifact as group- or world-writable. It recorded no model request. The verified sidecar is mode 0755. Hosted Node is the only other launch artifact, which points to Node as the cause. The original run did not retain its exact mode. The TestOps reporter retained the infrastructure failure. **Expected behavior** The manual path must capture the job-run and launch IDs from the active official watch session. It must validate those IDs against the same GitHub run, job, launch and standing plan before provider work. A requested concurrency value must fit the configured ceiling and the selected runner cap. The default must use the lower ceiling. Before credentials, the worker must stage the same Node bytes in an owned directory and preserve Runner permission and digest admission. **Steps to reproduce** 1. Dispatch the trusted Runner protocol workflow with reporting enabled and an empty job-run service input. 2. Let `allurectl watch` create the registration session. 3. Observe that the original helper rejects the empty child input before selection validation. ## What Changed - Call the supported `allurectl job-run env` command for manual registration. Use a fixed argument list without a shell and bound its time and output. - Parse only numeric job-run and launch IDs. Reject missing, duplicate, malformed or conflicting values. Discard all other command output. - Bind the captured launch ID to the authoritative job-run response. Keep the service-supplied ID path and its existing checks. - Apply requested parallelism within both configured and physical ceilings. Use their minimum as the default. Keep the selected-case capacity and provider budget checks. - Snapshot the same current Linux Node bytes into a fresh owner-only directory before credentials. Verify the copy, execute it to bind its path and version, and put only that directory first on the job-local path. Record original and copied Node modes and hashes plus the actual sidecar mode and hash. Keep shared cache permissions and Runner security checks intact. - Add focused context, identity, concurrency, permission and secret-exclusion controls. Supply the production physical cap in the existing release-workflow fixture. - Fix the existing Kimi cancellation fixture to await the complete readiness sentinel within the same 100 by 10 ms bound. Keep its cancellation and cleanup assertions. ## Verification - The original empty-ID failure reproduces locally on the actual helper with all 41 standing cases and zero job-run API reads. - All 63 Node publication, projection, metrics, permission and workflow-security tests pass. The repaired manual context flows through the actual selection helper to case 194. Wrong launch, duplicate IDs, malformed values and command failures still stop selection. - The original catalog prefix rejects configured 100 / hosted 57 / requested 2. The repaired prefix admits 2, clamps a missing request to 57, keeps legacy 100, and rejects requests above either ceiling. - The full release-policy CI command passes locally: 72 release-workflow tests and 107 companion tests. The first sandbox run could not bind its loopback fixtures; the same tests passed with local-network access. The prior-head CI fixture omission is retained and fixed by supplying the same physical cap as production. - Both full static Node file groups pass locally: 418 passed and 11 existing opt-in skips, at concurrency 2. Initial runs used stale dependency links and failed. Correcting only isolated ignored links to declared cached dependencies and pnpm 9.15.4 removed those setup failures. Local Node 26.4 differs from CI Node 24; the exact-head CI below provides canonical verification. - `actionlint .github/workflows/runner-protocol-live-evals.yml` and `git diff --check` pass. - The new staging helper passes all four tests as a nonroot Linux user with networking disabled. An actual retained Runner comparison reproduces the qualified-artifact rejection with Node mode 0777. The same Node bytes at 0500 in a 0700 directory pass that launch boundary and reach a closed provider session. The local turn still fails without credentials or network; this is permission proof, not a protocol case pass. Local Linux Node 24.21 differs from the hosted 24.22.1 executable. - Exact final head `8e06ee5611` [repository CI](https://github.com/paperclipai/paperclip/actions/runs/41892896405) completed successfully. Typecheck, build and all four Runner lanes passed. Runner package tests passed 4950 tests in 297 files, with 10 existing skips. Fresh exact-head Greptile review is 5/5 with no actionable findings or open threads. Prior `59378f` CI Kimi empty-sentinel race and canceled chat worker remain retained. - The patched manual workflow has not run against live TestOps. A separate service-triggered probe supplies its own ID. The service probe used the original concurrency guard with a temporary configured ceiling of 57. After the probe, the original repository ceiling 100 was restored with no active campaigns. That probe does not verify this patched concurrency code. The next launch waits for the protected merge and source/lock verification. ## Risks An incompatible official CLI output fails closed. The helper never evaluates the output or records its unknown variables. The existing job, workflow URL, source, plan and provider checks remain required. An omitted concurrency request now uses the lower of the configured and physical ceilings. Explicit requests above either ceiling still fail. The worker now executes a private copy of its existing Node binary. A copy, mode, hash, path, version or sidecar mismatch stops before credential access. The copy does not make dynamically linked Homebrew Node portable; this workflow path is Linux only. This change does not alter the provider roster, policy, limits, retries or result grading. ## Model Used OpenAI Codex, GPT-6. The exact hosted model ID and context window are not exposed in this session. Used reasoning, code editing, repository inspection and tool execution. ## 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 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
512 lines
28 KiB
JavaScript
512 lines
28 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { createHash } from "node:crypto";
|
|
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { spawnSync } from "node:child_process";
|
|
import { tmpdir } from "node:os";
|
|
import { resolve } from "node:path";
|
|
import test from "node:test";
|
|
|
|
const repositoryRoot = resolve(import.meta.dirname, "../../..");
|
|
const workflowPath = resolve(
|
|
repositoryRoot,
|
|
".github/workflows/runner-protocol-live-evals.yml",
|
|
);
|
|
const trustedPrWorkflowPath = resolve(
|
|
repositoryRoot,
|
|
".github/workflows/pr-trusted.yml",
|
|
);
|
|
|
|
test("Grok subscription credentials require explicit catalog selection and observed auth evidence", async () => {
|
|
const workflow = await readFile(workflowPath, "utf8");
|
|
const paid = workflow.slice(workflow.indexOf(" steps: &direct_eval_steps"), workflow.indexOf(" eval_shard_1:"));
|
|
assert.match(workflow, /grok_authentication:\n[\s\S]*?type: choice\n[\s\S]*?default: api_key/u);
|
|
assert.match(workflow, /--grok-authentication "\$GROK_AUTHENTICATION"/u);
|
|
assert.ok(paid.includes("PAPERCLIP_ACPX_GROK_AUTH_JSON_SECRET: ${{ matrix.credentialName == 'PAPERCLIP_ACPX_GROK_AUTH_JSON_SECRET' && secrets.GROK_AUTH_JSON || '' }}"));
|
|
assert.ok(paid.includes("XAI_API_KEY: ${{ matrix.credentialName == 'XAI_API_KEY' && secrets.XAI_API_KEY || '' }}"));
|
|
assert.match(paid, /--summary-path cell-output\/roster-summary\.json/u);
|
|
assert.match(paid, /JSON\.parse\(readFileSync\("cell-output\/roster-summary\.json", "utf8"\)\)\.authenticationMode/u);
|
|
assert.match(paid, /authenticationMode !== expected/u);
|
|
for (const job of [workflow.slice(0, workflow.indexOf(" eval_shard_0:")), workflow.slice(workflow.indexOf(" report:"))]) {
|
|
assert.doesNotMatch(job, /secrets\.GROK_AUTH_JSON/u);
|
|
}
|
|
});
|
|
|
|
test("direct live eval workflow keeps paid execution behind stable actor authorization", async () => {
|
|
const workflow = await readFile(workflowPath, "utf8");
|
|
assert.match(workflow, /^\s{2}authorize:/mu);
|
|
assert.match(workflow, /RUNNER_E2E_ALLOWED_ACTOR_IDS/u);
|
|
assert.match(workflow, /github\.actor_id/u);
|
|
assert.match(workflow, /github\.triggering_actor/u);
|
|
assert.match(workflow, /refs\/heads\/\$DEFAULT_BRANCH/u);
|
|
assert.match(workflow, /Reauthorize paid execution before provider access/u);
|
|
const copilotCredential = "COPILOT_GITHUB_TOKEN: ${{ matrix.credentialName == 'COPILOT_GITHUB_TOKEN' && secrets.COPILOT_GITHUB_TOKEN || '' }}";
|
|
assert.equal(workflow.split(copilotCredential).length - 1, 1);
|
|
const paid = workflow.slice(workflow.indexOf(" eval_shard_0:"), workflow.indexOf(" eval_shard_1:"));
|
|
assert.ok(paid.includes("'runner-e2e-private-paid' || 'runner-e2e-paid'"));
|
|
assert.ok(paid.includes(copilotCredential));
|
|
const reauthorizationIndex = paid.indexOf("Reauthorize paid execution before provider access");
|
|
assert.ok(reauthorizationIndex >= 0);
|
|
assert.ok(reauthorizationIndex < paid.indexOf(copilotCredential));
|
|
assert.match(
|
|
workflow,
|
|
/Reauthorize paid execution before provider access[\s\S]*actions\/checkout@[0-9a-f]{40}[\s\S]*Run one immutable direct protocol cell/u,
|
|
);
|
|
assert.doesNotMatch(
|
|
workflow,
|
|
/^\s{2}(?:pull_request|pull_request_target|push|workflow_call|workflow_run):/mu,
|
|
);
|
|
const actions = [
|
|
...workflow.matchAll(/^\s*(?:-\s*)?uses:\s*([^\s#]+)/gmu),
|
|
].map((match) => match[1]);
|
|
assert.ok(actions.length > 0);
|
|
for (const action of actions) assert.match(action, /^[^@]+@[0-9a-f]{40}$/u);
|
|
});
|
|
|
|
test("pull request CI builds the canonical Evalbook viewer", async () => {
|
|
const workflow = await readFile(trustedPrWorkflowPath, "utf8");
|
|
const buildJob = workflow.slice(
|
|
workflow.indexOf(" build:"),
|
|
workflow.indexOf(" verify_serialized_server:"),
|
|
);
|
|
|
|
assert.match(
|
|
buildJob,
|
|
/name: Build Runner Evalbook viewer[\s\S]*pnpm --filter @paperclipai\/paperclip-runner build:issue-thread/u,
|
|
);
|
|
});
|
|
|
|
test("resolves both repositories immutably and bounds total matrix concurrency", async () => {
|
|
const workflow = await readFile(workflowPath, "utf8");
|
|
const targetLock = workflow.slice(
|
|
workflow.indexOf(" target_lock:"),
|
|
workflow.indexOf(" catalog:"),
|
|
);
|
|
assert.match(targetLock, /ref: \$\{\{ needs\.authorize\.outputs\.target_sha \}\}/u);
|
|
assert.match(targetLock, /pnpm install --ignore-scripts --no-frozen-lockfile --lockfile-only/u);
|
|
assert.match(targetLock, /Upload resolved target lockfile/u);
|
|
const build = workflow.slice(
|
|
workflow.indexOf(" build_runner:"),
|
|
workflow.indexOf(" eval_shard_0:"),
|
|
);
|
|
assert.match(build, /Download resolved target lockfile/u);
|
|
assert.match(build, /Restore resolved target lockfile/u);
|
|
const authorize = workflow.slice(
|
|
workflow.indexOf(" authorize:"),
|
|
workflow.indexOf(" catalog:"),
|
|
);
|
|
assert.match(authorize, /repos\/\$REPOSITORY\/branches\/\$encoded_branch/u);
|
|
assert.match(authorize, /\^\[0-9a-f\]\{40\}\$/u);
|
|
assert.match(
|
|
authorize,
|
|
/repos\/paperclipai\/paperclip-evals\/commits\/\$EVALS_SHA/u,
|
|
);
|
|
assert.match(authorize, /COMMITPERCLIP_KEY/u);
|
|
assert.match(authorize, /GH_REPO: paperclipai\/paperclip-evals/u);
|
|
assert.match(
|
|
authorize,
|
|
/GH_TOKEN: \$\{\{ steps\.evals_token\.outputs\.value \}\}/u,
|
|
);
|
|
assert.match(authorize, /test "\$resolved" = "\$EVALS_SHA"/u);
|
|
const catalog = workflow.slice(
|
|
workflow.indexOf(" catalog:"),
|
|
workflow.indexOf(" build_runner:"),
|
|
);
|
|
assert.match(
|
|
catalog,
|
|
/ref: \$\{\{ needs\.authorize\.outputs\.evals_sha \}\}/u,
|
|
);
|
|
assert.match(catalog, /RUNNER_E2E_MAX_PARALLEL/u);
|
|
assert.match(catalog, /max_parallel_per_shard/u);
|
|
const privateCheckouts = [
|
|
...workflow.matchAll(
|
|
/repository: paperclipai\/paperclip-evals[\s\S]*?persist-credentials: false/gmu,
|
|
),
|
|
];
|
|
assert.equal(privateCheckouts.length, 4);
|
|
const privateTokenSteps = [
|
|
...workflow.matchAll(
|
|
/^ - name: Generate private eval-repository token\n(?<body>(?:^ {8,}.*\n?)*)/gmu,
|
|
),
|
|
];
|
|
assert.equal(privateTokenSteps.length, 5);
|
|
for (const tokenStep of privateTokenSteps) {
|
|
assert.match(
|
|
tokenStep.groups.body,
|
|
/^ {10}GH_REPO: paperclipai\/paperclip-evals$/mu,
|
|
"every private-eval token must be minted from the eval repository installation",
|
|
);
|
|
}
|
|
for (const checkout of privateCheckouts) {
|
|
assert.match(
|
|
checkout[0],
|
|
/token: \$\{\{ steps\.evals_token\.outputs\.value \}\}/u,
|
|
);
|
|
}
|
|
assert.match(workflow, /matrix_0/u);
|
|
assert.match(workflow, /matrix_1/u);
|
|
assert.match(
|
|
workflow,
|
|
/pnpm --filter @paperclipai\/paperclip-runner deploy --prod/u,
|
|
);
|
|
assert.match(
|
|
workflow,
|
|
/runner_cli=runner-protocol-build\/extracted\/portable\/dist\/cli\/eval-session\.js/u,
|
|
);
|
|
assert.doesNotMatch(workflow, /npm install --prefix/u);
|
|
});
|
|
|
|
test("publishes only the separately sanitized Evalbook through trusted OIDC code", async () => {
|
|
const workflow = await readFile(workflowPath, "utf8");
|
|
const report = workflow.slice(
|
|
workflow.indexOf(" report:"),
|
|
workflow.indexOf(" publish_history:"),
|
|
);
|
|
assert.match(
|
|
report,
|
|
/Render the access-controlled canonical Evalbook report/u,
|
|
);
|
|
assert.match(
|
|
report,
|
|
/--viewer-root runner-protocol-build\/extracted\/dist-issue-thread/u,
|
|
);
|
|
assert.match(report, /runner-protocol-eval-campaign\.mjs sanitize/u);
|
|
assert.match(
|
|
report,
|
|
/Upload legacy canonical Evalbook and raw attempts/u,
|
|
);
|
|
assert.match(report, /Upload publisher-only sanitized Evalbook/u);
|
|
assert.match(
|
|
report,
|
|
/verify-runner-evalbook-viewer\.mjs --report-root runner-protocol-merged\/public-report/u,
|
|
);
|
|
assert.match(
|
|
report,
|
|
/verify-runner-evalbook-viewer\.mjs --report-root runner-protocol-merged\/report/u,
|
|
);
|
|
assert.match(
|
|
report,
|
|
/--viewer-root runner-protocol-build\/extracted\/dist-issue-thread\s*\\\n\s*--public-viewer/u,
|
|
);
|
|
assert.equal([...report.matchAll(/--viewer-root /gu)].length, 2);
|
|
|
|
const publisher = workflow.slice(workflow.indexOf(" publish_history:"));
|
|
assert.match(publisher, /ref: \$\{\{ github\.sha \}\}/u);
|
|
assert.match(publisher, /id-token: write/u);
|
|
assert.match(publisher, /runner-protocol-eval-public-/u);
|
|
assert.match(publisher, /publish-runner-protocol-eval-history\.mjs/u);
|
|
assert.match(publisher, /runner-protocol-evals/u);
|
|
assert.match(publisher, /runner-protocol-viewer-/u);
|
|
assert.match(publisher, /PAPERCLIP_RUNNER_PROTOCOL_EVAL_VIEWER_DIR/u);
|
|
assert.match(publisher, /url: \$\{\{ steps\.publish\.outputs\.report_url \}\}/u);
|
|
assert.match(publisher, /Publish versioned report and refresh the root index\n\s+id: publish/u);
|
|
assert.doesNotMatch(publisher, /(?:OPENAI|ANTHROPIC|OPENROUTER)_API_KEY/u);
|
|
assert.doesNotMatch(publisher, /paperclipai\/paperclip-evals/u);
|
|
assert.doesNotMatch(publisher, /downloaded-runner-protocol-evals/u);
|
|
});
|
|
|
|
test("provisions the Codex userns profile before any provider credentials", async () => {
|
|
const workflow = await readFile(workflowPath, "utf8");
|
|
const direct = workflow.slice(
|
|
workflow.indexOf(" steps: &direct_eval_steps"),
|
|
workflow.indexOf(" eval_shard_1:"),
|
|
);
|
|
const profile = direct.indexOf("Provision Codex sandbox on the disposable trusted runner");
|
|
const credentials = direct.indexOf("Prepare short-lived AgentCore web identity");
|
|
assert.ok(profile >= 0 && credentials >= 0 && profile < credentials);
|
|
assert.match(direct, /apparmor_restrict_unprivileged_userns/u);
|
|
assert.match(direct, /apparmor_parser/u);
|
|
assert.match(direct, /userns,/u);
|
|
assert.match(direct, /runner-protocol-build\/extracted\/portable/u);
|
|
assert.match(direct, /matrix\.provider == 'codex'/u);
|
|
assert.match(direct, /matrix\.rosterId == 'protocol-live-acpx-codex-control'/u);
|
|
assert.doesNotMatch(direct, /matrix\.profileId/u);
|
|
const build = workflow.slice(
|
|
workflow.indexOf(" build_runner:"),
|
|
workflow.indexOf(" eval_shard_0:"),
|
|
);
|
|
assert.match(build, /Materialize the pinned OpenCode executable before packaging/u);
|
|
assert.match(build, /materialize-opencode-binary\.mjs/u);
|
|
});
|
|
|
|
test("report preparation stays on the trusted lock and install mode", async () => {
|
|
const workflow = await readFile(workflowPath, "utf8");
|
|
const report = workflow.slice(
|
|
workflow.indexOf(" report:"),
|
|
workflow.indexOf(" publish_history:"),
|
|
);
|
|
assert.match(report, /ref: \$\{\{ github\.sha \}\}/u);
|
|
assert.doesNotMatch(report, /Download resolved target lockfile/u);
|
|
assert.doesNotMatch(report, /Restore resolved target lockfile/u);
|
|
assert.doesNotMatch(report, /--no-frozen-lockfile|cache: pnpm/u);
|
|
assert.match(report, /pnpm install --frozen-lockfile --ignore-scripts --store-dir "\$RUNNER_TEMP\/runner-report-pnpm-store"/u);
|
|
});
|
|
|
|
test("trusted catalog, direct eval, and report orchestration stay on workflow revision", async () => {
|
|
const workflow = await readFile(workflowPath, "utf8");
|
|
for (const [job, next] of [
|
|
[" catalog:", " build_runner:"],
|
|
[" eval_shard_0:", " eval_shard_1:"],
|
|
[" report:", " publish_history:"],
|
|
]) {
|
|
const section = workflow.slice(workflow.indexOf(job), workflow.indexOf(next));
|
|
assert.match(section, /ref: \$\{\{ github\.sha \}\}/u, `${job} must use the trusted workflow revision`);
|
|
assert.doesNotMatch(section, /ref: \$\{\{ needs\.authorize\.outputs\.target_sha \}\}/u, `${job} must not execute target orchestration code`);
|
|
}
|
|
});
|
|
|
|
|
|
test("authentication failures retain cell metadata without leaking malformed summary content", async () => {
|
|
const workflow = await readFile(workflowPath, "utf8");
|
|
const script = workflow.match(/CELL_EXIT_CODE="\$status" node --input-type=module <<'NODE'\n([\s\S]*?) NODE/u)?.[1];
|
|
assert.ok(script);
|
|
const root = await mkdtemp(resolve(tmpdir(), "grok-cell-evidence-"));
|
|
try {
|
|
await mkdir(resolve(root, "cell-output"));
|
|
const summary = resolve(root, "cell-output/roster-summary.json");
|
|
for (const [content, expectedFailure, expectedMode] of [
|
|
[undefined, "grok_authentication_evidence_unreadable", undefined],
|
|
["{SENSITIVE_SENTINEL", "grok_authentication_evidence_unreadable", undefined],
|
|
["null", "grok_authentication_evidence_unreadable", undefined],
|
|
['{"authenticationMode":"SENSITIVE_SENTINEL"}', "grok_authentication_evidence_mismatch", undefined],
|
|
['{"authenticationMode":"api_key"}', "grok_authentication_evidence_mismatch", "api_key"],
|
|
['{"authenticationMode":"subscription"}', undefined, "subscription"],
|
|
]) {
|
|
if (content === undefined) await rm(summary, { force: true });
|
|
else await writeFile(summary, content);
|
|
const result = spawnSync(process.execPath, ["--input-type=module", "-e", script], {
|
|
cwd: root, encoding: "utf8",
|
|
env: { CREDENTIAL_NAME: "PAPERCLIP_ACPX_GROK_AUTH_JSON_SECRET", CELL_ID: "cell-1", CASE_ID: "context", ROSTER_FILE: "grok.json", CELL_EXIT_CODE: "7" },
|
|
});
|
|
assert.equal(result.status, expectedFailure ? 1 : 0);
|
|
const retained = await readFile(resolve(root, "cell-output/cell.json"), "utf8");
|
|
const metadata = JSON.parse(retained);
|
|
assert.equal(metadata.cellId, "cell-1");
|
|
assert.equal(metadata.caseId, "context");
|
|
assert.equal(metadata.exitCode, 7);
|
|
assert.equal(metadata.authenticationEvidenceFailure, expectedFailure);
|
|
assert.equal(metadata.authenticationMode, expectedMode);
|
|
assert.doesNotMatch(retained + result.stdout + result.stderr, /SENSITIVE_SENTINEL/u);
|
|
}
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
function workflowShell(workflow, name) {
|
|
const step = workflow.slice(workflow.indexOf(` - name: ${name}\n`)).split(/\n - /u)[0];
|
|
assert.ok(step.startsWith(` - name: ${name}\n`));
|
|
const script = step.split(' run: |\n')[1];
|
|
assert.ok(script);
|
|
return script.split('\n').map(line => line.slice(10)).join('\n');
|
|
}
|
|
|
|
test('controlled admission rejects missing policy, automatic retries and job reruns before paid work', async () => {
|
|
const workflow = await readFile(workflowPath, 'utf8');
|
|
const script = workflowShell(workflow, 'Validate accounting mode and retry envelope');
|
|
const root = await mkdtemp(resolve(tmpdir(), 'runner-workflow-admission-'));
|
|
try {
|
|
for (const [mode, event, attempt, retries, policy, status] of [
|
|
['controlled', 'workflow_dispatch', '1', '0', '', 1],
|
|
['controlled', 'workflow_dispatch', '2', '0', JSON.stringify({schema:'paperclip.runner-eval.hosted-policy/v1',controllerLockSha256:'e'.repeat(64)}), 1],
|
|
['controlled', 'workflow_dispatch', '1', '1', JSON.stringify({schema:'paperclip.runner-eval.hosted-policy/v1',controllerLockSha256:'e'.repeat(64)}), 1],
|
|
['legacy', 'schedule', '1', '1', '', 1],
|
|
['controlled', 'workflow_dispatch', '1', '0', JSON.stringify({schema:'paperclip.runner-eval.hosted-policy/v1',controllerLockSha256:'e'.repeat(64)}), 0],
|
|
['legacy', 'workflow_dispatch', '1', '1', '', 0],
|
|
]) {
|
|
const result = spawnSync('bash', ['-c', script], { encoding: 'utf8', env: { PATH: process.env.PATH,
|
|
ACCOUNTING_MODE: mode, EVENT_NAME: event, GITHUB_RUN_ATTEMPT: attempt, RETRIES: retries,
|
|
CONTROLLER_POLICY_JSON: policy, TARGET_SHA: 'a'.repeat(40), GITHUB_SHA: 'a'.repeat(40), GITHUB_OUTPUT: resolve(root, 'output') } });
|
|
assert.equal(result.status, status, `${mode}/${event}/${attempt}/${retries}: ${result.stderr}`);
|
|
}
|
|
} finally { await rm(root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('controlled workers keep the required GitHub-hosted origin even when the legacy AWS fleet is enabled', async () => {
|
|
const workflow = await readFile(workflowPath, 'utf8');
|
|
const script = workflowShell(workflow.slice(0, workflow.indexOf('\n target_lock:')), 'Select paid test runner');
|
|
const root = await mkdtemp(resolve(tmpdir(), 'runner-controlled-origin-'));
|
|
try {
|
|
for (const [mode, aws, runner] of [['controlled', 'true', 'ubuntu-latest'], ['controlled', 'false', 'ubuntu-latest'],
|
|
['legacy', 'true', 'runs-on/fleet=paperclip-public-pr-x64/env=public-ci'], ['legacy', 'false', 'ubuntu-latest']]) {
|
|
const output = resolve(root, `${mode}-${aws}`);
|
|
const result = spawnSync('bash', ['-euc', script], { encoding: 'utf8', env: { PATH: process.env.PATH,
|
|
ACCOUNTING_MODE: mode, AWS_PAID_RUNNER_ENABLED: aws, GITHUB_OUTPUT: output } });
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.match(await readFile(output, 'utf8'), new RegExp(`^runner=${runner}$`, 'm'));
|
|
}
|
|
} finally { await rm(root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('requested parallelism respects both configured capacity and the selected runner cap', async () => {
|
|
const workflow = await readFile(workflowPath, 'utf8');
|
|
const script = workflowShell(workflow, 'Build the two bounded roster-plus-case matrices');
|
|
const prefix = script.slice(0, script.indexOf('node packages/paperclip-runner/scripts/runner-protocol-eval-campaign.mjs'));
|
|
assert.ok(prefix.includes('REQUESTED_MAX_PARALLEL'));
|
|
for (const [configured, limit, requested, expected] of [['100', '57', '2', '2'], ['100', '57', '', '57'],
|
|
['100', '100', '', '100'], ['8', '57', '16', null], ['100', '57', '58', null], ['100', '57', '1', null]]) {
|
|
const result = spawnSync('bash', ['-euc', `${prefix}\nprintf '%s' "$MAX_PARALLEL"`], { encoding: 'utf8', env: {
|
|
PATH: process.env.PATH, MAX_PARALLEL: configured, MAX_PARALLEL_LIMIT: limit, REQUESTED_MAX_PARALLEL: requested } });
|
|
assert.equal(result.status === 0, expected !== null, `${configured}/${limit}/${requested}: ${result.stderr}`);
|
|
if (expected !== null) assert.equal(result.stdout, expected);
|
|
}
|
|
});
|
|
|
|
test('the provider uses an owned same-byte Node before credentials without changing shared host permissions', async () => {
|
|
const workflow = await readFile(workflowPath, 'utf8');
|
|
const paid = workflow.slice(workflow.indexOf(' steps: &direct_eval_steps'), workflow.indexOf(' eval_shard_1:'));
|
|
const staging = paid.indexOf('Stage an owned provider Node before credentials');
|
|
assert.ok(staging > paid.indexOf('Verify and extract portable runner'));
|
|
assert.ok(staging < paid.indexOf('Obtain private archive credentials for transfer steps only'));
|
|
assert.ok(staging < paid.indexOf('Prepare short-lived AgentCore web identity'));
|
|
assert.ok(staging < paid.indexOf('Run one immutable direct protocol cell'));
|
|
const command = workflowShell(paid, 'Stage an owned provider Node before credentials');
|
|
assert.match(command, /node packages\/paperclip-runner\/scripts\/stage-runner-provider-node\.mjs/u);
|
|
assert.match(command, /"\$RUNNER_TEMP\/runner-protocol-provider-node"/u);
|
|
assert.match(command, /portable\/dist\/cli\/acpx-runtime-sidecar\.cjs/u);
|
|
assert.match(command, /cell-output\/provider-node-receipt\.json/u);
|
|
assert.doesNotMatch(command, /secrets\.|\bsudo\b|\bchmod\b|hostedtoolcache/u);
|
|
});
|
|
|
|
test('TestOps admission preserves the selected mode gates, current source and dedicated roster', async () => {
|
|
const workflow = await readFile(workflowPath, 'utf8');
|
|
const script = workflowShell(workflow, 'Validate accounting mode and retry envelope');
|
|
const root = await mkdtemp(resolve(tmpdir(), 'runner-testops-admission-'));
|
|
try {
|
|
for (const [mode, source, roster, policy, retries, expected] of [
|
|
['legacy', 'a', 'protocol-live-testops-claude', true, '0', 0],
|
|
['controlled', 'a', 'protocol-live-testops-claude', true, '0', 0],
|
|
['controlled', 'a', 'protocol-live-testops-claude', false, '0', 1],
|
|
['controlled', 'a', 'protocol-live-testops-claude', true, '1', 1],
|
|
['controlled', 'b', 'protocol-live-testops-claude', true, '0', 1],
|
|
['legacy', 'b', 'protocol-live-testops-claude', true, '0', 1],
|
|
['legacy', 'a', 'all', true, '0', 1],
|
|
]) {
|
|
const result = spawnSync('bash', ['-c', script], { encoding: 'utf8', env: { PATH: process.env.PATH,
|
|
ACCOUNTING_MODE: mode, EVENT_NAME: 'workflow_dispatch', GITHUB_RUN_ATTEMPT: '1', RETRIES: retries,
|
|
TESTOPS_REQUESTED: 'true', REQUESTED_ROSTERS: roster,
|
|
CONTROLLER_POLICY_JSON: policy ? JSON.stringify({schema:'paperclip.runner-eval.hosted-policy/v1',controllerLockSha256:'e'.repeat(64)}) : '',
|
|
TARGET_SHA: source.repeat(40), GITHUB_SHA: 'a'.repeat(40), GITHUB_OUTPUT: resolve(root, 'output') } });
|
|
assert.equal(result.status, expected, `${mode}/${source}/${roster}: ${result.stderr}`);
|
|
}
|
|
} finally { await rm(root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('TestOps receives only a closed projection created inside the report boundary', async () => {
|
|
const workflow = await readFile(workflowPath, 'utf8');
|
|
const report = workflow.slice(workflow.indexOf(' report:'), workflow.indexOf(' testops_report:'));
|
|
const publisher = workflow.slice(workflow.indexOf(' testops_report:'), workflow.indexOf(' publish_history:'));
|
|
const publicationCondition = publisher.match(/^ if: (.+)$/mu)?.[1];
|
|
assert.ok(publicationCondition);
|
|
const admits = (cancelled, enabled = true, selected = true) => Function('always', 'cancelled', 'needs',
|
|
`return ${publicationCondition}`)(() => true, () => cancelled,
|
|
{ authorize: { outputs: { testops_enabled: String(enabled) } }, testops_selection: { result: selected ? 'success' : 'failure' } });
|
|
assert.equal(admits(false), true);
|
|
assert.equal(admits(true), false);
|
|
assert.equal(admits(false, false), false);
|
|
assert.equal(admits(false, true, false), false);
|
|
assert.match(report, /environment:.*runner-e2e-private-history/u);
|
|
assert.match(report, /Create the closed TestOps projection inside the report boundary[\s\S]*runner-protocol-testops\.mjs export/u);
|
|
assert.match(report, /Transfer only the closed TestOps results and receipt[\s\S]*path: runner-protocol-testops-projection\//u);
|
|
assert.match(publisher, /name: runner-protocol-testops-projection-/u);
|
|
assert.doesNotMatch(publisher, /runner-protocol-eval-public-|runner-protocol-merged|private-history|AWS_ACCESS_KEY_ID|BRAINTRUST_API_KEY/u);
|
|
assert.match(publisher, /steps\.projection\.outcome != 'success'/u);
|
|
});
|
|
|
|
test('controlled capacity counts the filtered selection before any provider work', async () => {
|
|
const workflow = await readFile(workflowPath, 'utf8');
|
|
const filter = workflow.indexOf(' - name: Apply the exact selected protocol cases before fan-out');
|
|
const limit = workflow.indexOf(' - name: Enforce controlled limits against the selected catalog');
|
|
const upload = workflow.indexOf(' - name: Upload immutable campaign catalog');
|
|
assert.ok(filter > 0 && filter < limit && limit < upload);
|
|
const script = workflowShell(workflow, 'Enforce controlled limits against the selected catalog');
|
|
const cell = { provider: 'acpx', acpxAgent: 'claude', driver: 'acpx_runtime' };
|
|
const root = await mkdtemp(resolve(tmpdir(), 'runner-selected-capacity-'));
|
|
try {
|
|
for (const [cells, maximum, expected] of [[[cell], 1, 0], [Array(41).fill(cell), 1, 1],
|
|
[[cell, cell], 1, 1], [[{ ...cell, provider: 'unsupported' }], 1, 1], [[cell], 0, 1]]) {
|
|
await writeFile(resolve(root, 'runner-protocol-eval-catalog.json'), JSON.stringify({ cells }));
|
|
const result = spawnSync('bash', ['-euc', script], { cwd: root, encoding: 'utf8', env: { PATH: process.env.PATH,
|
|
CONTROLLER_POLICY_JSON: JSON.stringify({ maxCells: maximum }) } });
|
|
assert.equal(result.status === 0, expected === 0, result.stderr);
|
|
}
|
|
} finally { await rm(root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('controlled evidence uses private storage and cloud credentials stay out of provider execution', async () => {
|
|
const workflow = await readFile(workflowPath, 'utf8');
|
|
const paid = workflow.slice(workflow.indexOf(' steps: &direct_eval_steps'), workflow.indexOf(' eval_shard_1:'));
|
|
const model = paid.slice(paid.indexOf(' - name: Run one immutable direct protocol cell'), paid.indexOf(' - name: Retain controlled cell evidence'));
|
|
assert.ok(paid.indexOf('Verify current worker admission before provider credentials') < paid.indexOf('Run one immutable direct protocol cell'));
|
|
assert.ok(paid.indexOf('Provision Codex sandbox') < paid.indexOf('Obtain private archive credentials'));
|
|
assert.doesNotMatch(model, /AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY|AWS_SESSION_TOKEN/u);
|
|
assert.match(model, /runner_cli="\$root\/runtime\/controlled-evals\/run.mjs"/u);
|
|
for (const step of workflow.split(/\n - /u).filter(step => /uses: actions\/upload-artifact/u.test(step))) {
|
|
if (/path: (?:cell-output|runner-protocol-merged)\//u.test(step)) assert.match(step, /accounting_mode == 'legacy'/u);
|
|
assert.doesNotMatch(step, /path: .*runner-protocol-control\//u);
|
|
}
|
|
for (const step of workflow.split(/\n - /u).filter(step => /uses: aws-actions\/configure-aws-credentials/u.test(step) && step.includes('id: private_history_credentials'))) {
|
|
assert.match(step, /output-env-credentials: false/u);
|
|
assert.match(step, /disable-retry: true/u);
|
|
}
|
|
assert.match(workflow, /upload protocol-control/u);
|
|
assert.match(workflow, /upload "protocol-cell-\$CELL_ID"/u);
|
|
assert.match(workflow, /upload protocol-report/u);
|
|
assert.match(workflow, /--controller-manifest "\$PAPERCLIP_RUNNER_EVAL_CONTROL_MANIFEST"/u);
|
|
});
|
|
|
|
test('Braintrust-controlled workers do not require an unused direct provider credential', async () => {
|
|
const workflow = await readFile(workflowPath, 'utf8');
|
|
const script = workflowShell(workflow, 'Run one immutable direct protocol cell');
|
|
const credentialCheck = script.slice(script.indexOf('if [ "$ACCOUNTING_MODE"'), script.indexOf('if [ "$PROVIDER"'));
|
|
assert.ok(credentialCheck.includes('test -n "${!CREDENTIAL_NAME:-}"'));
|
|
for (const [mode, credential, status] of [['controlled', '', 0], ['legacy', '', 1], ['legacy', 'synthetic-only', 0]]) {
|
|
const result = spawnSync('bash', ['-euc', credentialCheck], { encoding: 'utf8', env: {
|
|
ACCOUNTING_MODE: mode, CREDENTIAL_NAME: 'OPENAI_API_KEY', OPENAI_API_KEY: credential,
|
|
} });
|
|
assert.equal(result.status, status, result.stderr);
|
|
}
|
|
});
|
|
|
|
test('private archive jobs consume verified controller code without resolving dependencies', async () => {
|
|
const workflow = await readFile(workflowPath, 'utf8');
|
|
const builder = workflow.slice(workflow.indexOf(' build_controller:'), workflow.indexOf(' prepare_controller:'));
|
|
const preparer = workflow.slice(workflow.indexOf(' prepare_controller:'), workflow.indexOf(' eval_shard_0:'));
|
|
assert.doesNotMatch(builder, /id-token: write|environment:|secrets\./u);
|
|
assert.match(builder, /build-runtime.mjs/u);
|
|
assert.doesNotMatch(builder, /--no-frozen-lockfile/u);
|
|
assert.match(builder, /test "\$RESOLVED_LOCK_SHA256" = "\$EXPECTED_LOCK_SHA256"/u);
|
|
assert.match(builder, /pnpm install --frozen-lockfile --ignore-scripts --store-dir/u);
|
|
assert.doesNotMatch(preparer, /pnpm install|build-runtime.mjs|cache: pnpm/u);
|
|
assert.ok(preparer.indexOf('verify-runtime.mjs') < preparer.indexOf('hosted.mjs" prepare'));
|
|
assert.match(preparer, /needs\.build_controller\.outputs\.runtime_receipt_sha256/u);
|
|
for (const job of workflow.split(/\n(?= [A-Za-z0-9_]+:)/u).filter(value => value.includes('id-token: write'))) {
|
|
assert.doesNotMatch(job, /--no-frozen-lockfile|cache: pnpm|actions\/cache/u);
|
|
for (const install of job.matchAll(/pnpm install[^\n]*/gu)) {
|
|
assert.match(install[0], /--frozen-lockfile/u);
|
|
assert.match(install[0], /--ignore-scripts/u);
|
|
assert.match(install[0], /--store-dir/u);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('controller dependency lock rejects resolution or byte drift before installation', async () => {
|
|
const workflow = await readFile(workflowPath, 'utf8');
|
|
const script = workflowShell(workflow, 'Build portable controller in an unprivileged job');
|
|
const pin = script.slice(script.indexOf('test "$RESOLVED_LOCK_SHA256"'), script.indexOf('pnpm install'));
|
|
const root = await mkdtemp(resolve(tmpdir(), 'runner-reviewed-lock-'));
|
|
const digest = createHash('sha256').update('reviewed dependencies').digest('hex');
|
|
try {
|
|
await mkdir(resolve(root, 'runner-controller-lock'));
|
|
for (const [index, resolved, bytes, status] of [[0, digest, 'reviewed dependencies', 0], [1, 'f'.repeat(64), 'reviewed dependencies', 1], [2, digest, 'changed dependencies', 1]]) {
|
|
const cwd = resolve(root, `worker-${index}`); await mkdir(cwd);
|
|
await writeFile(resolve(root, 'runner-controller-lock/pnpm-lock.yaml'), bytes);
|
|
const result = spawnSync('bash', ['-euc', pin], { cwd, encoding: 'utf8', env: {
|
|
PATH: process.env.PATH, RUNNER_TEMP: root, EXPECTED_LOCK_SHA256: digest, RESOLVED_LOCK_SHA256: resolved,
|
|
} });
|
|
assert.equal(result.status, status, result.stderr);
|
|
if (status === 0) assert.equal(await readFile(resolve(cwd, 'pnpm-lock.yaml'), 'utf8'), bytes);
|
|
else await assert.rejects(readFile(resolve(cwd, 'pnpm-lock.yaml')), { code: 'ENOENT' });
|
|
}
|
|
} finally { await rm(root, { recursive: true, force: true }); }
|
|
});
|