mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 19:28:46 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The Runner executes agents through native and managed provider drivers. > - The direct live eval layer had drifted from the current Runner contracts. > - The old local workflow did not provide a complete parallel campaign or durable report history. > - The Runner also needed current native OpenCode and OpenRouter qualification. > - This pull request restores the direct campaign, corrects the runtime gaps that the campaign found, and adds safe hosted Evalbook history. > - The benefit is repeatable model comparison against an immutable Runner and eval source revision. ## Linked Issues or Issue Description Refs #11297 Refs #11634 **What existing behavior does this improve?** This improves the direct live `paperclip-runner` eval workflow, provider execution contract, and static Evalbook reporting path. **Current behavior** The direct evals do not have one maintained full campaign on current `master`. OpenCode has no qualified multi-model OpenRouter roster. Parallel provider bursts can compact committed events before the transport observes them. Local reports do not have a separate safe S3 history index. **Proposed behavior** Run one immutable roster-plus-case matrix. Use the shared paid AWS runner fleet. Keep raw artifacts access-controlled. Publish a sanitized canonical Evalbook report under the separate `runner-protocol-evals` S3 prefix. Keep immutable campaign directories plus root history, latest, and latest-green pointers. **Reason and benefit** Maintainers can compare native Codex, native OpenCode, ACPX, Claude Managed, and AWS AgentCore behavior over time. They can inspect failures without mixing this direct protocol layer with browser full-stack E2E. **Breaking changes** None. The new workflow and S3 prefix are additive. The existing Runner full-stack E2E workflow and report remain separate. ## What Changed - Added a trusted two-shard direct live workflow for up to 393 roster-plus-case cells. - Reused the numeric actor allowlist, protected paid environment, and RunsOn fleet controls from Runner full-stack E2E. - Added immutable Runner and eval revision resolution, exact credential boundaries, bounded retries, and cost ceilings. - Added a public report projection that removes sessions, transcripts, tool payloads, state, traces, raw failures, remote profile identities, and credential-shaped values. - Added additive S3 history under `runner-protocol-evals`, with immutable campaigns and mutable root index pointers. - Added native OpenCode model injection and current OpenRouter pricing contracts. - Fixed direct eval completion, workflow execution, semantic discovery, warm-attach state reset, executable binding, and event-burst handling. - Kept Runner browser full-stack E2E behavior and publication separate. - Documented local and hosted direct eval operation. ## Verification - `pnpm --filter @paperclipai/paperclip-runner test:runner-protocol-eval-publish` — 15 passed. - `pnpm --filter @paperclipai/paperclip-runner build:typescript` — passed. - `actionlint .github/workflows/runner-protocol-live-evals.yml .github/workflows/runner-full-stack-e2e.yml` — passed. - Local current matrix at the revision in [paperclip-evals#17](https://github.com/paperclipai/paperclip-evals/pull/17) — 323 cells across 10 enabled configurations completed. - Final local current matrix — 269 passed, 11 behavior failures, and 43 expected macOS-only ACPX platform failures. - Targeted Runner checks — 13/13 eval-session tests, 15/15 publisher/security tests, and package typecheck passed; complete PR CI is green, including all browser E2E shards. ## Risks - Paid live campaigns can consume provider budget. Actor authorization, exact per-cell ceilings, protected environments, and explicit schedule enablement bound this risk. - Public reports can leak provider data. The workflow publishes only a separately projected report and validates every file before upload. - The new workflow cannot publish until it is present on the default branch. This pull request does not change the existing `runner-full-stack-e2e` publication path. - The campaign is large. It uses two GitHub matrices and caps combined concurrency at the shared fleet limit. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used OpenAI Codex on GPT-5. The exact deployment ID and context-window size are not exposed. The model used reasoning, code editing, browser inspection, repository tools, and live provider 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 (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
110 lines
4.0 KiB
JavaScript
110 lines
4.0 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFile } from "node:fs/promises";
|
|
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",
|
|
);
|
|
|
|
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);
|
|
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("resolves both repositories immutably and bounds total matrix concurrency", async () => {
|
|
const workflow = await readFile(workflowPath, "utf8");
|
|
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_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, 3);
|
|
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);
|
|
});
|
|
|
|
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 access-controlled canonical Evalbook and raw attempts/u,
|
|
);
|
|
assert.match(report, /Upload publisher-only sanitized Evalbook/u);
|
|
|
|
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.doesNotMatch(publisher, /(?:OPENAI|ANTHROPIC|OPENROUTER)_API_KEY/u);
|
|
assert.doesNotMatch(publisher, /paperclipai\/paperclip-evals/u);
|
|
assert.doesNotMatch(publisher, /downloaded-runner-protocol-evals/u);
|
|
});
|