Files
PaperClipAI/tests/runner-e2e/pi-file-evidence.ts
T
Nicky LeachandClaude Opus 5.5 4c1f629c31 test(evals): repair the Runner support suite after master drift (#19529)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The Runner support suite (`pnpm test:e2e:runner:unit`) tests the
code behind paid Runner qualification: workflow security policy,
evidence oracles, cost receipts and controller recovery.
> - PR CI does not run this suite. Only a manual
`docker-runner-check.yml` run does.
> - So deliberate changes on master broke its tests, and nobody saw it.
There were 12–13 failures on Nov 3 and 16 on Nov 6.
> - This pull request finds the master change behind each failure and
fixes the side that is wrong. No security check becomes weaker.
> - The benefit is that the suite passes on master again (with #19527),
so it can become a required check.

## Linked Issues or Issue Description

Refs #18516, #18625, #18983, #18325, #19265, #18994, #17009, #19195.
Companion PR: #19527 (the one workflow fix).

**What happened?**

At master `543aa4d331`, `pnpm test:e2e:runner:unit` had 16 failures in 8
files. At `98ebeefbc9` I saw 15 in 7 files. The two embedded-Postgres
files passed in every run I made, and the 256MiB packaging test timed
out once.

**Expected behavior**

The suite passes on master. Each test still checks the rule it was
written for.

**Steps to reproduce**

Run `pnpm install`, then `pnpm test:e2e:runner:unit` on master.

## What Changed

Each failure, its cause, and the fix:

- **`workflow-security.test.ts`: chat catalog workflows hold provider
keys and use `runner-e2e-private-paid`.** This is a deliberate policy.
#18516 admitted guarded, pinned-source chat TestOps cells, with special
checks. #18625 removed the pilot workflows and their checks. It did not
move the checks to the full-catalog workflows from #18542 and #18647,
which use the same trust model. The fix adds `chat-catalog-testops.yml`
and `chat-catalog-testops-case.yml` to the provider-key allowlist. It
also brings back the #18516 checks, adapted to the caller plus
reusable-workflow layout:
- The cell is `workflow_call` only. Its single caller is the master-only
dispatcher.
- The trusted guard runs before the pinned candidate checkout.
`verify-selection` runs before each provider key.
- The paid job never sees `ALLURE_TOKEN`. The report job runs in
`runner-e2e-private-history` and has no candidate checkout. The upload
job has no private environment and no secret except `ALLURE_TOKEN`.
- Provider keys appear only in the two caller jobs. Those jobs need
`authorize, select, build` and pass the guard's `target_sha`.
- I mutation-tested this: an `ALLURE_TOKEN` in the paid job fails, and a
second caller workflow fails.
- **`workflow-security.test.ts`: `ref: ${{ github.sha }}` 6 vs 4,
`persist-credentials: false` 12 vs 10.** #18983 added two trusted
TestOps jobs (`testops_selection`, `publish_testops`). Both check out
reviewed code next to the TestOps token. This is the safe direction. The
counts are updated. A new check makes sure both jobs check out only
`github.sha`, do not install the candidate lockfile, and use no secret
except `ALLURE_TOKEN`.
- **`pi-file-evidence.test.ts` (4 tests): "one observed provider
validation execution".** #19265 deliberately changed the ACPX tool
normalizer to keep the latest concrete title. Pi's shell call now goes
from `bash` to the exact command. The ACPX sidecar uses this normalizer,
so the oracle would also reject real Pi runs. The oracle now accepts one
change only, from `bash` to the exact validation command, with no change
back. It also counts every completed `execute` call, whatever its name.
This is stricter than before: a refined title cannot hide a second shell
command. New mutations cover a regressed name, a different command, and
an extra call that has its own command name. The retained 44-row real
stream still passes.
- **`pi-controls-evidence.test.ts`: Pi profile 37.** #18325 deliberately
reset Pi to profile 37. The pin and the `pi-native` suite hash are
updated. I checked that the old hash comes back exactly when the version
is set to 36. So the version is the only change.
- **`controller-outage.test.ts`: 1 continuity cell, not 2.** #18994
deliberately limited this lane to providers with managed warm sessions.
The test now expects exactly `runner-codex`.
- **`operating-receipts.test.ts` (3 tests): "Operating overhead
allowance needs review".** This is a time bomb from #17009. The fixture
allowance was valid from Oct 1 until Nov 1, 2026. `admitAttempt` reads
the real clock. The allowance window is limited to 31 days, so a longer
fixture is not possible. These tests now pin only `Date` to the fixture
time. Timers and the filesystem stay real. I ran the full suite with the
clock moved to Dec 15, 2026, and found no other date-bound failures.
- **New file `private-trust-workflow-security.test.ts`.** The private
archive trust test, the chat catalog checks and the full-stack TestOps
job checks moved here from `workflow-security.test.ts`. That file would
have crossed 1,000 lines. It is now 970 lines. The checks did not
change.
- **`remote-native-bootstrap.test.ts`: cannot load
`@paperclipai/plugin-sdk`.** Since #19195, server code under test
imports the built plugin SDK. `run-unit.mjs` now runs
`scripts/ensure-plugin-build-deps.mjs` first, as `test:run:general`
already does. This step is cached.
- **`private-framework-report.test.ts`: 256MiB packaging test timed
out.** It takes about 10s alone and more than 30s with the whole suite
running in parallel. It now has a 120s budget.
- **`controller-database.test.ts`,
`controller-recovery-observer.integration.test.ts`: no change.**
Embedded Postgres started and both files passed in all my runs. The
earlier failures look like local resource problems.

## Verification

- Combined check (this branch plus #19527, on master `5d25117ffa`):
`pnpm test:e2e:runner:unit` — Vitest 317 files passed (6,815 tests, 3
skipped), node:test 373 passed, 0 failed. It took 63s on a 10-core Mac.
- `pnpm test:e2e:runner:typecheck` — passed.
- This branch alone fails only the two pnpm-setup tests that #19527
fixes.
- Focused files: `pi-file-evidence` 75 passed, `pi-controls-evidence` 72
passed, `operating-receipts` 12 passed, `workflow-security` 14 of 16
passed (the other 2 pass with #19527), `private-trust-workflow-security`
3 passed, `remote-native-bootstrap` 71 passed.

## Risks

- Low risk. Only test files, one evidence oracle under
`tests/runner-e2e/`, and the suite launcher change. No product code
changes.
- The Pi oracle change affects how paid Pi file qualification is graded.
It accepts the old and new title shapes, and it is stricter about extra
shell calls. A live Pi qualification run would confirm it on real
Rust-normalized events. I did not run one, because it is paid.
- The chat catalog policy is the same as #18516. Under that design, the
pinned candidate source runs in `runner-e2e-private-paid` with
`id-token: write`. The guards above limit who can start it and which
source runs. This PR does not change that trust model.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), by Anthropic, in Claude Code with
tool use (shell, file edits, git, GitHub CLI).

## 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

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 11:26:44 -08:00

233 lines
19 KiB
TypeScript

import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import type { Page } from "@playwright/test";
import type { RunnerApi } from "./api.js";
import { isValidNativePrpEnvelope } from "./native-event-envelope.js";
type Row = Record<string, any>;
const rec = (value: unknown): Row => value !== null && typeof value === "object" && !Array.isArray(value) ? value as Row : {};
const sha = (value: string | Buffer) => createHash("sha256").update(value).digest("hex");
const requireEvidence = (value: unknown, reason: string) => { if (!value) throw new Error(`Pi file evidence: ${reason}`); };
export function piFileContract(nonce: string) {
if (!/^[A-Za-z0-9-]{1,100}$/u.test(nonce)) throw new Error("Invalid file fixture nonce");
const filename = `extended-${nonce}.txt`, before = `ready-${nonce}\n`, after = `verified-${nonce}\n`;
const validationMarker = `PI-VALIDATED-${nonce}`;
const validationCommand = `node -e 'if(require("node:fs").readFileSync(${JSON.stringify(filename)},"utf8")!==${JSON.stringify(after)})throw Error("mismatch");console.log(${JSON.stringify(validationMarker)})'`;
if ([...validationCommand].length > 240) throw new Error("Validation command exceeds native Rust tool-name bound");
return { filename, before, after, sha256: sha(after), byteSize: Buffer.byteLength(after), validationMarker, validationCommand };
}
export function piFilePrompt(nonce: string) {
const c = piFileContract(nonce);
return [
`The fixture has already created ${c.filename} in your execution workspace with exactly ${JSON.stringify(c.before)}. Read it, then use your native edit tool once to replace those bytes with exactly ${JSON.stringify(c.after)}. Do not substitute write, bash, or an API for the edit.`,
`Run this exact native bash command exactly once, without an added shell prefix or suffix:\n\`\`\`bash\n${c.validationCommand}\n\`\`\`\nIt independently reads the file, fails on incorrect bytes, and prints the validation marker only after success. This is the only bash call allowed for this task; do not run any other shell commands.`,
`Deliver the edited text file as a downloadable attachment using the assigned register_deliverable semantic tool, with filename and workspace-relative contentRef ${c.filename}, contentType text/plain, byteSize ${c.byteSize}, sha256 ${c.sha256}, title ${c.filename}, and a fresh idempotencyKey. Use the supplied byteSize and sha256 for the exact validated post-edit bytes; do not run extra commands to obtain metadata. Wait for the real registration receipt. Do not upload a different file or claim publication from a disk path.`,
`After successful validation and registration, call paperclip_finish with reportedWorkDisposition done, summary EXTENDED-FILE-${nonce}, the current completion contract revision and satisfied objective criterion, no remaining work, the actual validation command/result, and the registered artifact reference.`,
`Wait for paperclip_finish to succeed, then emit exactly EXTENDED-FILE-${nonce} as your final response. Do not create unrelated files or work.`,
].join("\n");
}
export async function seedPiFile(workspace: string, nonce: string) {
const c = piFileContract(nonce);
await writeFile(join(workspace, c.filename), c.before, { flag: "wx" });
const bytes = await readFile(join(workspace, c.filename));
requireEvidence(bytes.equals(Buffer.from(c.before)), "seed bytes changed before provider startup");
return { schema: "paperclip.e2e.pi-file-seed.v1", filename: c.filename, beforeSha256: sha(bytes), byteSize: bytes.length };
}
export function gradePiCopyback(run: Row, lease: Row, companyId: string, environmentId: string) {
const context = rec(run.contextSnapshot), environment = rec(context.paperclipEnvironment);
const reference = rec(rec(run.runnerProfileJson).nativeWorkspaceSync);
const binding = rec(rec(rec(run.runnerProfileJson).nativeExecutionInput).binding);
requireEvidence(run.companyId === companyId && reference.schema === "paperclip.native-workspace-sync/v2"
&& reference.state === "finalized" && [reference.baselineSha256, reference.descriptorSha256, reference.finalHostSha256]
.every(value => typeof value === "string" && /^[a-f0-9]{64}$/u.test(value)), "finalized production copy-back hashes required");
requireEvidence(typeof reference.workspaceId === "string" && reference.workspaceId.length > 0
&& reference.workspaceId === context.executionWorkspaceId
&& (binding.executionWorkspaceId === undefined || binding.executionWorkspaceId === reference.workspaceId)
&& environment.id === environmentId && environment.driver === "sandbox" && environment.leaseId === reference.leaseId,
"copy-back workspace and environment must belong to the actual run");
requireEvidence(lease.id === reference.leaseId && lease.companyId === companyId && lease.environmentId === environmentId
&& lease.heartbeatRunId === run.id && lease.provider === "daytona" && typeof reference.providerLeaseId === "string"
&& reference.providerLeaseId.length > 0 && lease.providerLeaseId === reference.providerLeaseId
&& typeof reference.remoteCwd === "string" && reference.remoteCwd.startsWith("/")
&& reference.remoteCwd === rec(lease.metadata).remoteCwd, "copy-back must bind the exact run and provider lease");
return { kind: "product_copyback", reference, lease: { id: lease.id, companyId, environmentId, heartbeatRunId: run.id,
provider: lease.provider, providerLeaseId: lease.providerLeaseId, remoteCwd: reference.remoteCwd },
guestObservation: false, hostSnapshotRecomputed: false };
}
/** Only the common public projection is authority here. Native raw arguments,
* full diff blocks and typed command exit codes are deliberately not invented.
* The native structured output receipt is distinct from that nullable typed field. */
export function gradePiFileEvidence(input: {
nonce: string; companyId: string; issueId: string; agentId: string; run: Row;
environment: "local" | "daytona"; environmentId: string; lease?: Row;
events: unknown[]; seed: unknown; workspaceBytes: Buffer; attachments: unknown[];
attachmentId: string; downloadedBytes: Buffer; activity: unknown[];
}) {
const c = piFileContract(input.nonce), run = input.run, seed = rec(input.seed);
const workspaceProvenance = input.environment === "daytona"
? gradePiCopyback(run, rec(input.lease), input.companyId, input.environmentId)
: { kind: "independent_local_workspace" };
requireEvidence(run.id && run.companyId === input.companyId && run.agentId === input.agentId
&& run.runtimeMode === "native" && run.status === "succeeded", "actual native run identity");
requireEvidence(seed.schema === "paperclip.e2e.pi-file-seed.v1" && seed.filename === c.filename
&& seed.beforeSha256 === sha(c.before) && seed.byteSize === Buffer.byteLength(c.before), "independent pre-edit seed");
requireEvidence(input.workspaceBytes.equals(Buffer.from(c.after)), "independent final workspace bytes");
requireEvidence(input.events.length > 0 && input.events.length <= 20_000, "bounded run events");
const rows = input.events.map(rec).filter(row => rec(row.payload).prpEvent !== undefined);
const seen = new Set<string>(), seqs = new Set<number>(), sourceSeqs = new Map<string, number>();
const canonical = rows.map(row => {
const e = rec(rec(row.payload).prpEvent);
requireEvidence(row.companyId === input.companyId && row.runId === run.id && e.runId === run.id
&& isValidNativePrpEnvelope(e, row.protocolSchemaVersion) && row.eventType === e.eventType
&& Number.isSafeInteger(row.seq) && row.seq > 0 && !seqs.has(row.seq)
&& typeof e.sourceInstanceId === "string" && e.sourceInstanceId.length > 0
&& Number.isSafeInteger(e.sourceSeq) && e.sourceSeq > (sourceSeqs.get(e.sourceInstanceId) ?? 0)
&& row.sourceInstanceId === e.sourceInstanceId && row.sourceSeq === e.sourceSeq && row.sourceEventId === e.sourceEventId
&& e.sourceEventId === `${e.sourceInstanceId}:${e.runId}:${e.sourceSeq}` && !seen.has(e.sourceEventId), "foreign, duplicated, or reordered durable evidence");
seqs.add(row.seq); seen.add(e.sourceEventId); sourceSeqs.set(e.sourceInstanceId, e.sourceSeq);
return { row, e, p: rec(e.payload) };
});
const tools = canonical.filter(x => x.e.eventType.startsWith("tool.execution.") && x.p.transport === "builtin");
const edits = tools.filter(x => x.e.eventType === "tool.execution.completed" && x.p.operation === "edit");
requireEvidence(edits.length === 1, "one native edit required; final-only writes cannot qualify");
const edit = edits[0]!;
requireEvidence(edit.p.name === "edit" && edit.p.target === c.filename, "exact native edit and relative target required");
// `names[0]` is the opening name. Later names may only refine it once, to
// the final name (the runner keeps the latest concrete ACP title, #19265).
function lifecycle(end: typeof edit, names: readonly unknown[] = [end.p.name]) {
const matches = tools.filter(x => x.p.executionId === end.p.executionId);
const starts = matches.filter(x => x.e.eventType === "tool.execution.started");
requireEvidence(typeof end.p.executionId === "string" && end.p.executionId.length > 0
&& end.p.schema === "paperclip.tool.execution.v1" && end.p.status === "completed"
&& end.e.sourceKind === "runner" && end.e.sourceInstanceId === run.runnerInstanceId
&& end.e.normalizedSessionId === run.nativeSessionId && typeof end.e.turnId === "string" && end.e.turnId.length > 0
&& typeof end.e.normalizedSessionId === "string" && end.e.normalizedSessionId.length > 0
&& starts.length === 1 && starts[0]!.row.seq < end.row.seq
&& matches.filter(x => x.e.eventType === "tool.execution.completed").length === 1
&& matches.every(x => x.p.schema === end.p.schema && names.includes(x.p.name) && x.p.operation === end.p.operation
&& x.row.seq >= starts[0]!.row.seq && x.row.seq <= end.row.seq
&& x.e.sourceKind === "runner" && x.e.turnId === end.e.turnId
&& x.e.normalizedSessionId === end.e.normalizedSessionId && x.e.sourceInstanceId === end.e.sourceInstanceId
&& (x === end || (x.p.status === "running"
&& ["tool.execution.started", "tool.execution.progressed"].includes(x.e.eventType)))), "incomplete or unsuccessful native tool lifecycle");
// Native arguments arrive incrementally. An unresolved target is allowed
// only before the first exact target; conflicting or regressed targets fail.
let resolved = false;
for (const x of matches) {
requireEvidence(x.p.target === end.p.target || (!resolved && x.p.target === null), "conflicting native tool target");
if (x.p.target !== null) resolved = true;
}
let refined = false;
for (const x of matches) {
requireEvidence(!refined || x.p.name === end.p.name && x.p.name !== names[0], "conflicting or regressed native tool name");
if (x.p.name !== names[0]) refined = true;
}
return { start: starts[0]!, matches };
}
lifecycle(edit);
// Count every completed shell execution, whatever its name, so a refined
// title cannot hide a second command from the one-bash-call rule.
const validations = tools.filter(x => x.e.eventType === "tool.execution.completed" && x.p.operation === "execute");
requireEvidence(validations.length === 1, "one observed provider validation execution");
const validationNames = ["bash", c.validationCommand] as const;
requireEvidence(validationNames.includes(validations[0]!.p.name as never), "exact native validation command required");
const validation = validations[0]!, { start, matches } = lifecycle(validation, validationNames);
requireEvidence(start.row.seq > edit.row.seq && validation.e.turnId === edit.e.turnId
&& validation.e.normalizedSessionId === edit.e.normalizedSessionId
&& validation.e.sourceInstanceId === edit.e.sourceInstanceId && validation.p.outputTruncated === false,
"validation must follow the edit in the same native turn");
// The lifecycle opens as bash. The resolved command is retained as
// progress, and may also become the name once the runner sees it.
const commandProgress = `${c.validationCommand} (in_progress): ${c.validationCommand}`;
const allowedProgress = new Set([`bash (pending): [terminal] ${validation.p.executionId}`,
`${c.validationCommand} (pending): ${c.validationCommand}`, commandProgress]);
requireEvidence(matches.some(x => x !== validation && x.p.progress === commandProgress)
&& matches.every(x => x.p.progress === null || allowedProgress.has(x.p.progress)),
"exact correlated command progress before completion required");
requireEvidence(typeof validation.p.output === "string" && Buffer.byteLength(validation.p.output) <= 16_384,
"bounded native validation receipt required");
let output: Row = {};
try { output = rec(JSON.parse(validation.p.output)); } catch { /* Rejected below; plain marker text is insufficient. */ }
const structured = rec(output.structuredContent), expectedOutput = `${c.validationMarker}\n`;
requireEvidence(structured.exit_code === 0 && structured.truncated === false && structured.output === expectedOutput
&& Array.isArray(output.content) && output.content.length === 1
&& rec(output.content[0]).type === "text" && rec(output.content[0]).text === expectedOutput
&& (validation.p.exitCode === null || validation.p.exitCode === 0), "successful native structured validation receipt required");
const attachments = input.attachments.map(rec).filter(a => a.id === input.attachmentId);
requireEvidence(attachments.length === 1, "one public attachment");
const a = attachments[0]!;
requireEvidence(a.companyId === input.companyId && a.issueId === input.issueId && a.originatingRunId === run.id
&& a.createdByAgentId === input.agentId && a.originalFilename === c.filename && a.contentType === "text/plain"
&& a.byteSize === c.byteSize && a.sha256 === c.sha256, "attachment bytes and immutable run attribution");
requireEvidence(input.downloadedBytes.equals(Buffer.from(c.after)), "actual public download bytes");
const receipts = Object.values(rec(rec(run.resultJson).semanticToolReceipts)).map(rec)
.filter(r => r.operationId === "register_deliverable" && rec(r.input).filename === c.filename);
requireEvidence(receipts.length === 1, "one authenticated semantic registration");
const receipt = receipts[0]!, args = rec(receipt.input), result = rec(receipt.result);
requireEvidence(args.contentRef === c.filename && args.contentType === "text/plain" && args.byteSize === c.byteSize
&& args.sha256 === c.sha256 && result.disposition === "applied" && result.commandId === `deliverable-prepared:${a.id}`
&& Array.isArray(result.entityRefs) && result.entityRefs[0] === a.id, "registered artifact receipt must match downloaded attachment");
const publications = input.activity.map(rec).filter(row => row.action === "issue.attachment_added"
&& rec(row.details).attachmentId === a.id);
requireEvidence(publications.length === 1 && publications[0]!.companyId === input.companyId
&& publications[0]!.runId === run.id && publications[0]!.actorId === input.agentId
&& publications[0]!.entityId === input.issueId && rec(publications[0]!.details).source === "paperclip_runner_protocol",
"publication activity must belong to this run and task");
return {
schema: "paperclip.e2e.pi-file-evidence.v1", passed: true, workspaceProvenance,
runId: run.id, turnId: edit.e.turnId, nativeEditExecutionId: edit.p.executionId,
validationExecutionId: validation.p.executionId, attachmentId: a.id,
verification: { kind: "independent_exact_bytes", beforeSha256: sha(c.before), afterSha256: c.sha256,
downloadedSha256: sha(input.downloadedBytes), byteSize: c.byteSize, providerExecutionStatus: validation.p.status,
command: c.validationCommand, providerToolName: validation.p.name, commandEvidence: "correlated_native_progress",
exitEvidence: "native_structured_output_receipt", nativeStructuredExitCode: structured.exit_code,
typedExitCode: validation.p.exitCode, nativeFileTarget: edit.p.target,
nativeFileAttribution: "workspace_relative_display_target" },
diff: { source: "independent_seed_and_workspace_bytes", path: c.filename,
text: `--- ${c.filename}\n+++ ${c.filename}\n@@ -1 +1 @@\n-${c.before.trimEnd()}\n+${c.after.trimEnd()}\n` },
limits: ["Exact command progress and the native structured output receipt are checked; raw arguments and nullable typed exitCode are not attested by those distinct fields.",
"Diff is computed from independently checked workspace bytes; it does not qualify native diff presentation or private invocation metadata."],
};
}
export async function collectPiFileEvidence(input: {
page: Page; api: RunnerApi; nonce: string; companyId: string; issueId: string; agentId: string;
run: Row; events: unknown[]; seed: unknown; workspace: string;
environment: "local" | "daytona"; environmentId: string;
evidence(data: unknown): Promise<void>;
}) {
const c = piFileContract(input.nonce);
let run = input.run, lease: Row | undefined;
if (input.environment === "daytona") {
run = await input.api.get<Row>(`/api/heartbeat-runs/${input.run.id}`);
requireEvidence(run.id === input.run.id, "public copy-back run readback changed identity");
const leaseId = rec(rec(run.runnerProfileJson).nativeWorkspaceSync).leaseId;
requireEvidence(typeof leaseId === "string" && /^[A-Za-z0-9_-]{1,128}$/u.test(leaseId), "copy-back lease reference missing");
lease = await input.api.get<Row>(`/api/environment-leases/${leaseId}`);
}
const attachments = await input.api.get<Row[]>(`/api/issues/${input.issueId}/attachments`);
const selected = attachments.filter(a => a.originalFilename === c.filename);
requireEvidence(selected.length === 1 && /^[a-f0-9-]{36}$/u.test(selected[0]!.id), "exact published file selection");
requireEvidence(selected[0]!.byteSize === c.byteSize && selected[0]!.sha256 === c.sha256
&& selected[0]!.contentType === "text/plain", "bounded attachment metadata before download");
const attachmentId = selected[0]!.id;
const contentPath = `/api/attachments/${attachmentId}/content`;
// Exercise the real task surface and the authenticated public download route.
await input.page.locator(`a[href*="${contentPath}"]`).first().waitFor({ state: "visible", timeout: 15_000 });
const response = await input.api.request.get(contentPath);
requireEvidence(response.ok(), "public attachment download failed");
const downloadedBytes = await response.body();
requireEvidence(downloadedBytes.length <= 4096, "bounded text attachment");
const workspaceBytes = await readFile(join(input.workspace, c.filename));
const activity = await input.api.get<Row[]>(`/api/issues/${input.issueId}/activity`);
await input.evidence({ schema: "paperclip.e2e.pi-file-observation.v1", attachments, attachmentId, activity,
workspaceBytes: workspaceBytes.toString("base64"), downloadedBytes: downloadedBytes.toString("base64"),
encoding: "base64", expectedSha256: c.sha256, copybackReference: rec(run.runnerProfileJson).nativeWorkspaceSync,
copybackLease: lease && { id: lease.id, companyId: lease.companyId, environmentId: lease.environmentId,
heartbeatRunId: lease.heartbeatRunId, provider: lease.provider, providerLeaseId: lease.providerLeaseId, remoteCwd: rec(lease.metadata).remoteCwd } });
return gradePiFileEvidence({ ...input, run, lease, attachments, attachmentId, downloadedBytes, workspaceBytes, activity });
}