mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-12 06:10:44 +01:00
## 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 <noreply@anthropic.com>
181 lines
10 KiB
Markdown
181 lines
10 KiB
Markdown
# 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:
|
|
|
|
1. Deploy this workflow and its helpers together on the protected default branch.
|
|
2. Keep the existing numeric actor allowlist and protected default-branch checks.
|
|
3. Keep `runner-e2e-private-paid` and `runner-e2e-private-history` restricted to
|
|
the default branch.
|
|
4. 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.
|
|
5. 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_dispatch` of this workflow on the default branch, with
|
|
the `manual` cadence. 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 `id` input.
|
|
|
|
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):
|
|
|
|
```json
|
|
{
|
|
"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.
|