## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Chat-feature evals run in the trusted `runner-full-stack-e2e` workflow and keep their evidence in private history > - A manual chat run needs authority to write that private evidence > - Today that authority is a stored approval for the exact run ID in `RUNNER_E2E_MANUAL_PRIVATE_APPROVALS_JSON` > - The run ID exists only after dispatch, so every manual run stops until a person writes the record > - The dispatcher is already checked against `RUNNER_E2E_ALLOWED_ACTOR_IDS`, so the second step adds friction without a new decision > - This pull request lets an allowlisted manual dispatch authorize its own private evidence, as the TestOps admission already does > - The benefit is that an allowlisted operator can start a chat eval and it runs, while scheduled runs still never touch chat evidence ## Linked Issues or Issue Description Refs #18134, Refs #18170 **What existing behavior does this improve?** A manual `runner-full-stack-e2e` run with chat-feature IDs fails at private evidence transfer unless a person first adds an approval row for that run ID to a repository variable. **Subsystem affected** Runner E2E manual workflow private evidence authorization (`manual-private-transfer.ts`, `runner-full-stack-e2e.yml`). **Proposed change** Derive the evidence authority from the dispatch itself, with the same checks the TestOps chat admission uses. ## What Changed - New `tests/runner-e2e/manual-dispatch-authority.ts`: `authorizeManualDispatch` admits private evidence only when all of these are true: - The event is `workflow_dispatch` of `runner-full-stack-e2e.yml` on the default branch, with the `manual` cadence. - The run attempt is `1`. - The actor is in `RUNNER_E2E_ALLOWED_ACTOR_IDS`, the event sender is that actor, and the triggering actor is the same person. - The trusted cell selection equals the dispatch's own `id` input. - The authority binds the workflow SHA, the target SHA from the authorize job, the run ID and the cells. Every job derives the same identity hash. - `manual-private-transfer.ts`: uses the TestOps authority first, then the dispatch authority, then the stored approval. With the dispatch authority, `prepare` records the target launcher digest instead of comparing it with an approved digest. - `runner-full-stack-e2e.yml`: the 14 steps that read `RUNNER_E2E_MANUAL_PRIVATE_APPROVALS_JSON` now set `PAPERCLIP_MANUAL_DISPATCH_AUTHORITY: v1` and pass `RUNNER_E2E_ALLOWED_ACTOR_IDS`. - `doc/manual-e2e-private-evidence.md`: describes the dispatch authority. The stored-approval shape stays documented for the TestOps catalog case workflow. - The new file is separate on purpose. `manual-private-retention.ts` and the other protocol files must match each target byte for byte, so existing candidate branches keep working. ## Verification - `vitest run tests/runner-e2e/manual-dispatch-authority.test.ts tests/runner-e2e/manual-private-transfer.test.ts tests/runner-e2e/manual-private-report.test.ts tests/runner-e2e/manual-private-report-integration.test.ts tests/runner-e2e/manual-private-policy.test.ts`: 57 tests passed. - New tests cover one valid dispatch and rejection of: a scheduled run, a bounded cadence, a non-allowlisted actor, an empty allowlist, a rerun by another person, a second attempt, a non-default or different workflow ref, a sender who is not the actor, a missing target SHA, a missing authority switch, and cells that differ from the dispatch input. The eight-cell limit still applies. - A `prepare` test runs the real transfer script: a non-allowlisted actor produces no outputs, and an allowlisted dispatch produces the session policy and an identity that records the target launcher digest. - Negative control: with the previous `manual-private-transfer.ts`, the new `prepare` test fails. - Workflow tests that read this workflow: 111 of 116 passed. The 5 failures in `workflow-target.test.ts` (`IMAGE_ONLY: unbound variable`) also fail on unchanged `master`. `paid-runner-routing.test.mjs` passed (7 tests). - I did not run a live dispatch. The first live proof is a manual dispatch of chat IDs after merge. ## Risks - The dispatcher now vouches for the target branch. A target branch with a changed launcher can write private evidence if an allowlisted operator dispatches it. The protocol files must still match the trusted copies byte for byte. - The eight-cell limit is unchanged. The report job has eight fixed read slots, and each read policy must fit the 2,048-character session policy limit. Lifting the limit needs a separate change to the report sessions. - The `runner-e2e-paid` environment still has required reviewers. That is a repository setting and this change does not touch it. - The authority uses `RUNNER_E2E_ALLOWED_ACTOR_IDS`, not `CODEOWNERS`. Keep that variable aligned with the people who may start paid runs. > 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 - Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code execution in Claude Code. ## 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 - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <[email protected]>
10 KiB
Restricted evidence for manual AWS E2E runs
The manual AWS workflow has a separate restricted evidence route. It uses the existing private-history bucket, protected environments, OIDC role and bounded archive transfer. It does not change the scheduled private-history lane.
GitHub Actions artifacts in a public repository are readable by repository readers. They are not an organization-private evidence store. The restricted route never uploads raw cell evidence or a merged report to Actions, S3 public history, or Pages. This stays true on setup failure, launcher failure and report failure. Previously staged files do not enable a public fallback.
Supported selections
An approved original run selects at most eight distinct execution IDs from
one fixed registry of the frozen 244 chat scenarios. All use profile
runner-opencode and environment local. The 242 catalog tasks retain their
exact area.case IDs. The two standalone scenarios retain their existing task
IDs: tasks.create-backlog maps to
chat-features.runner-opencode.local.create-backlog, and
tasks.delegate-announcement maps to
chat-features.runner-opencode.local.delegate-announcement. The dotted task
aliases are not registered. The exact registry is in
tests/runner-e2e/manual-private-retention.ts.
These IDs require the reviewed AWS Fleet route. Broad chat suites, mixed unsupported IDs, reruns and runs without dispatch authority fail closed. All chat-feature IDs are classified as restricted by trusted code. A target descriptor cannot opt out. Other manual selections keep the existing Actions route only after a trusted per-cell check confirms that the actual selected execution is not restricted. Selector whitespace and exclusivity follow the catalog producer.
The registry size does not increase the per-run limit or the eight fixed report read slots. Classification, cell binding, every approval row and session policy reject a private selection larger than eight before actionable outputs or archive credentials. Generic nonprivate catalog selection retains its existing semantics.
A registered ID is not a claim of feature readiness or permission to run it. Each target still needs the exact reviewed producer contracts, matching helper bytes, declared private inputs, fresh source fingerprints, an available original attempt and an allowlisted manual dispatch. Earlier accepted target branches do not acquire these bytes automatically. Running jobs keep their pinned sources. No retained grade or historical qualification transfers to a new target.
The private cell report contract binds the installed pinned Playwright producer and the selected cell before it accepts canonical static trace files. Default cell evidence keeps its existing unknown-carrier rules. Excluded installed build staging is outside the evidence tree; unknown carriers inside standard evidence paths still fail. A target with stronger ZIP or embedded-report scanning must retain those checks when it adapts this contract. Installed-target identities and runtime evidence remain separate from source-preparation metadata.
Deployment and operator gates
This source change alone does not configure a role, bucket, environment, target, or run. Before a supported run, the operator must:
- Deploy this workflow and its helpers together on the protected default branch.
- Keep the existing numeric actor allowlist and protected default-branch checks.
- Keep
runner-e2e-private-paidandrunner-e2e-private-historyrestricted to the default branch. - Provide the existing private-history role ARN, AWS region and bucket variables in those environments. The existing role must support the existing bounded prefix and bucket-policy, ownership, public-access and retention reads.
- Use a compatible target. Its private handoff and policy modules must match the workflow implementation byte for byte. Earlier target branches do not gain this protocol automatically.
Dispatch authority
In runner-full-stack-e2e.yml, the manual run itself authorizes its private
evidence. There is no separate per-run approval record. Trusted code in
tests/runner-e2e/manual-dispatch-authority.ts admits a cell only when all of
these are true:
- The event is
workflow_dispatchof this workflow on the default branch, with themanualcadence. Scheduled and bounded-cadence runs never qualify. - The run is on attempt
1. A rerun does not inherit authority. - The dispatching actor is in
RUNNER_E2E_ALLOWED_ACTOR_IDS, the event sender is that actor, and the triggering actor is the same person. - The trusted cell selection equals the dispatch's own
idinput.
The authority binds the workflow SHA, the target SHA resolved by the authorize job, the run ID and the cells. The dispatcher chooses and vouches for the target branch, so its launcher digest is recorded in the private identity instead of compared with a separately approved digest. Classification still happens before the protected job, and a failed check still stops before target dependency setup, archive credentials, model credentials or fixture secrets. The per-run limit of eight cells is unchanged.
Stored approval records
The TestOps catalog case workflow still passes a protected approval record as
MANUAL_PRIVATE_APPROVALS_JSON. That record has this closed shape (all values
below are placeholders):
{
"version": 1,
"approvals": [{
"workflowSha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"targetSha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"runId": "123456789",
"attempt": "1",
"executionIds": ["chat-features.runner-opencode.local.activity.agent-feedback"],
"protocol": "paperclip.manual-private-evidence/v1",
"launchSha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
}]
}
Trust and credential lifetime
The workflow checks out its own immutable github.sha into a separate directory.
It prepares pinned publication dependencies with --ignore-scripts and a
separate store before archive credentials. Transfers use that checkout's
absolute helper, tsx and archive-script paths. They do not load the target's
node_modules or an ambient Node loader. The target executes only after the
evidence authority and private-storage preflight pass.
The AWS action returns session credentials as step outputs, not job-wide environment exports. Only trusted preflight and transfer steps receive those outputs. Transfers require an explicit complete session and disable EC2 metadata fallback. A failed or expired session fails retention. It never enables an Actions upload. Exact-key session policies limit the intended transfer calls. Each current cell uses its own two-object read session; the report writer uses a separate session. Every generated policy is checked against the 2,048-character STS limit before the credential action. No wildcard object access is added.
This is operator-approved target execution, not a sandbox for malicious source. The protected job has OIDC permission. Its target launcher can inherit the GitHub OIDC request environment, and the existing harness copies its process environment into server/native children. A target that can request a new OIDC session may re-assume the role. An inline session policy does not prevent that. Reviewers must approve the complete exact target, not just its evidence flag. This change does not alter IAM, OIDC trust, host roles or credential duration.
Evidence and failure rules
The launcher keeps its ordinary standard evidence in an owned temporary root. It preserves the original framework result before launcher result updates. It also seals final packaged results after cleanup and campaign finalization. The transfer validates exact file inventories and hashes before archiving. It excludes instance databases, workspaces, credential registries, auth files, raw capture directories and arbitrary files outside the standard evidence scope. Free-form launcher console capture is not an approved retained carrier.
The existing credential scanners and PASS PNG, HTML, blob and snapshot checks
still apply. Original text, ZIP and media bytes receive those scans before
retention. Raw and encoded registered roots and provider-shaped names are also
rejected from original paths and inventories. Unknown extensions and SVG files
that the ordinary packager does not inspect cannot become complete originals.
Files cannot be silently dropped to meet a bound. Credential contamination,
unsupported carriers or a changed inventory stop the transfer. Missing required
PASS files still convert the attempt to infrastructure FAIL. The private inventory
retains all safe available originals and records requiredEvidenceComplete: false
and the exact missing list. Its complete field means the available file inventory
is complete; it does not certify PASS evidence completeness. Failed framework
attempts do not claim that PASS evidence checks ran. No file is synthesized and
no secret-bearing original is uploaded just because the bucket is private.
The existing archive bounds remain unchanged: at most 256 MiB of source files, 4,097 archive entries and 264 MiB transferred archive bytes. Duplicated original and packaged files count toward those bounds. A run that exceeds a bound does not have complete retained evidence and needs an explicit disposition.
Private objects use immutable conditional puts and version receipts under
runner-e2e-private/bounded-v1/campaigns/gha-RUN_ID-1/. Each cell has a hash-derived
object name. The report reads only the approved current cells and verifies each
archive against its retained transfer digest. Fresh merged HTML, blob, normalized output and report logs receive a final
standard-file scan and sealed inventory with the actual current report-step
credential roots, including its explicit AWS session. The report uses the same
raw, encoded-root and provider-shape checks before transfer. It retains the
scanned report in the same restricted store. A report error stays a report
error; retention does not change its grade or manufacture a public page.
Authenticated retrieval uses the existing private-history operator machinery and approved bucket access. This PR does not grant a reader role or perform an upload. Actual deployment, environment approval, compatible target fingerprints, private storage access and one exact run approval remain operational gates.