Files
PaperClipAI/tests/runner-e2e/manual-private-transfer.ts
T
Dotta 9c6993f0aa test(evals): pin corrected chat catalog and restore retained attachments (#19135)
## Thinking Path

> - Paperclip uses real Product E2E journeys to verify agent work.
> - The trusted TestOps controller must bind the reviewed source and
retain inspectable evidence.
> - The last uniform attempt exposed bounded observation and
evidence-retention defects. Its original outcomes remain unchanged.
> - The reviewed successor corrects those boundaries with the original
model, requests, counts, and clocks.
> - Its private framework report stores canonical attachments once. The
controller must validate and restore those references before normal
reporting.
> - This PR ports that reader support and pins the qualified source in
one infrastructure change. It does not merge the full catalog or its
core repair PRs.

## Linked Issues or Issue Description

Refs #19127, #19083, #19086, #19084, #19093, #19095, #19100, #19112.

**What happened?**

The last uniform attempt retained real failed outcomes. It also lost the
multipart-question original when evidence packaging exceeded its
unchanged physical byte allowance. Review found independent boundaries
in skill navigation, announcement observation, timeline attribution,
hire history, credential questions, and team visibility.

**Expected behavior**

The next run must execute the same ten requests against one reviewed
source. The controller must retain complete original evidence, validate
private attachment references, and report the actual outcome. Historical
failures and missing-original UNKNOWN grades must remain unchanged.

**Steps to reproduce**

Use the focused source controls linked above. The private framework
control uses a real failed Playwright test and a 140 MiB attachment.
Legacy duplicate carriers hit the old cap. Canonical original evidence
passes both seals and strict archive restoration. The controller
restores derived files and the existing TestOps exporter preserves the
failed result.

**Paperclip version/commit**

Qualified candidate: `155dd1bc1c4b85ae107f6a0cd429720caa281367`. The
controller port is based on current master. The full candidate catalog
stays on its own immutable branch.

**Deployment mode**

Existing AWS/TestOps job 100, plan 102, branch master. This PR does not
dispatch a run.

## What Changed

- Pin the published qualified source and its exact launcher and
descriptor fixtures.
- Preserve current manifest history. Update twelve declaration fields
across eleven cases: oracle versions and skill source-file provenance
only. Preserve all 244 identities, 242 canonical declarations, queries,
setup, human/native counts, clocks, profile, model, retry policy, and
global cap of 8.
- Port the exact source-owned compact framework helper and
private-transfer reader. Keep the three existing required
source/controller protocol helpers byte-identical.
- Validate both sealed inventories before restoring registered
attachments into the derived selected report tree. Verify exact bytes
and hashes. Preserve seals and unregistered evidence.
- Keep deep JSON, decoded-body, sensitive-key, path, and original/final
status checks. Plain-text bodies and registered plain files fail closed
if existing diagnostic redaction would change them; JSON remains under
structured inspection. Keep all existing physical byte, entry, ZIP,
time, privacy, and archive bounds.
- Preserve registered clean metadata and attachment bytes. Reject
content that needs privacy redaction instead of silently rewriting
immutable evidence.

## Verification

- 131 existing Node pin, selection, admission, adapter, reconciliation,
and security controls pass. The explicit new target assertion and drift
negatives remain active.
- 86 current-controller controls pass. They cover legacy reports,
privacy and static assets, canonical reference guards, both seals,
strict archive restoration, derived files, the actual report normalizer,
and the actual TestOps/Allure exporter. The original failed result stays
failed; screenshot and trace hashes resolve.
- Actual P1 controls reject both an unknown password in a decoded
plain-text body and an unknown command token in a registered text file.
Parent code fails both controls; corrected code passes. The composed
source passes 87 affected controls, fresh server declarations, and full
E2E typechecking. The old cfe head completed all 49 CI jobs without
adverse conclusions, but its review found this real privacy defect. That
CI result is not credited to this successor.
- The existing report-lock preflight returns already-reviewed at
`60f4c06bca47af3ab9d74281f2ef2e1ff559d566b862ba51c2fbe6c8e5bac35b`.
- The existing source SDK guard confirms 506 registered inputs,
unchanged registry
`efa70621e9fb42aa185f7ee616a1a726aa3eccb367e67e28817cedf26ef2ade8`, and
unchanged runtime pins. No registry renewal or SDK build was needed.
- Source launcher SHA-256:
`2b9a6953ee51f0ea0dbbd475b0491d9a6c39a1d6835eeaa912e0dbb7cf987724`.
Descriptor SHA-256:
`e026d5c000102f8d50397dc59055d187388304f91dbca9a6ccfbe747004cd16d`.
- No model call, evaluation dispatch, evidence download, reporting
retry, core merge, or permission change was performed. Full current-head
CI and review are pending. Separate source PR failures remain disclosed;
local controls do not establish universal source CI readiness.
- Seven cases have verified passing historical attempts. A uniform
passing ten-case run remains unverified. The old lost multipart-question
original remains UNKNOWN.

## Risks

This changes a trusted evidence reader and the source selected for
future measurements. The new helper is included in the controller import
closure. Compact references require matching originals; missing,
foreign, changed, unsafe, or redaction-required evidence fails closed.
Legacy reporter formats remain valid. A single canonical evidence set
can still exceed a bound. Current-head CI and material review must pass
before normal protected merge. Actual AWS/TestOps evidence remains
required afterward.

## Model Used

OpenAI `gpt-6.1-sol`, with reasoning, code editing, and tool use. The
host does not expose the context-window size.

## 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
2026-11-04 18:20:22 -06:00

231 lines
20 KiB
TypeScript

import { PRIVATE_FRAMEWORK_REPORT, validatePrivateFrameworkReport, materializePrivateFrameworkAttachments } from "./private-framework-report.js";
import { createHash } from "node:crypto";
import { appendFile, cp, lstat, mkdir, mkdtemp, readFile, readdir, realpath, writeFile } from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { canonicalGeneratedReportAsset, isTrustedGeneratedReportAsset, inspectCellReportProducer, privateCellEvidenceOwner, cellTraceAsset, readPrivateCellTraceAsset } from "./manual-private-report-assets.js";
import { sealManualResultEvidence, type ManualPrivateHandoff } from "./manual-private-handoff.js";
import { normalizedSecrets } from "./redaction.js";
import { CREDENTIAL_NAMES } from "./types.js";
import { isStandardEvidenceFile } from "./evidence.js";
import { BoundedHistoryPublisher } from "./history-budget.js";
import { privateArtifactKey, retainPrivateTransfer, transferPrivateArtifact, verifyPrivateHistory, type PrivateEvidenceCompleteness } from "./private-history.js";
import { authorizeManualPrivateEvidence, MANUAL_PRIVATE_PROTOCOL, manualCellKind, manualPrivateSessionPolicy } from "./manual-private-retention.js";
import { shippedChatEvidenceAuthority } from "./testops-chat-admission.js";
const hash = (bytes: string | Buffer) => createHash("sha256").update(bytes).digest("hex");
function request(env: NodeJS.ProcessEnv) {
const ids = JSON.parse(env.MANUAL_PRIVATE_IDS ?? "null") as string[];
const approval = env.PAPERCLIP_TESTOPS_CHAT_MAIN_ADMISSION !== undefined ? shippedChatEvidenceAuthority(env) : authorizeManualPrivateEvidence({ workflowSha: env.GITHUB_SHA ?? "", targetSha: env.TARGET_SHA ?? "",
runId: env.GITHUB_RUN_ID ?? "", attempt: env.GITHUB_RUN_ATTEMPT ?? "", executionIds: ids }, env.MANUAL_PRIVATE_APPROVALS_JSON ?? "");
return { ids, approval, bucket: env.RUNNER_E2E_PRIVATE_HISTORY_S3_BUCKET ?? "" };
}
async function output(name: string, value: string) {
if (/[\r\n]/.test(value)) throw new Error("Invalid private evidence step output");
await appendFile(process.env.GITHUB_OUTPUT!, `${name}=${value}\n`);
}
async function prepare(env: NodeJS.ProcessEnv) {
const { ids, approval, bucket } = request(env), id = env.SELECTED_EXECUTION_ID;
if (!id || !ids.includes(id)) throw new Error("Private evidence cell is not approved");
const target = await realpath(env.TARGET_ROOT ?? ""), trusted = path.resolve(import.meta.dirname, "../..");
if (target === trusted) throw new Error("Trusted transfer code must be separate from target checkout");
const launch = await readFile(path.join(target, "tests/runner-e2e/launch.ts"));
if (hash(launch) !== approval.launchSha256) throw new Error("Target evidence launcher differs from protected approval");
for (const file of ["manual-private-handoff.ts", "manual-private-retention.ts", "manual-private-report-assets.ts"]) {
if (!(await readFile(path.join(target, "tests/runner-e2e", file))).equals(await readFile(path.join(trusted, "tests/runner-e2e", file))))
throw new Error("Target private evidence protocol differs from trusted implementation");
}
const temporary = await realpath(env.RUNNER_TEMP ?? ""), root = await mkdtemp(path.join(temporary, "manual-private-evidence-"));
await writeFile(path.join(root, "identity.json"), JSON.stringify({ ...approval, executionIds: [id], root, status: "pending" }), { mode: 0o600, flag: "wx" });
await output("root", root); await output("session_policy", JSON.stringify(manualPrivateSessionPolicy({ bucket, runId: approval.runId, executionIds: [id], mode: "cell-write" })));
}
function requireSession(env: NodeJS.ProcessEnv) {
if (![env.AWS_ACCESS_KEY_ID, env.AWS_SECRET_ACCESS_KEY, env.AWS_SESSION_TOKEN].every(value => typeof value === "string" && value.length > 0))
throw new Error("Explicit private archive session is required; no credential fallback");
}
async function verify(env: NodeJS.ProcessEnv) {
requireSession(env); const { approval, bucket } = request(env), root = await realpath(env.PRIVATE_EVIDENCE_ROOT ?? "");
const identity = JSON.parse(await readFile(path.join(root, "identity.json"), "utf8"));
if (identity.approvalSha256 !== approval.approvalSha256 || identity.targetSha !== approval.targetSha || identity.workflowSha !== approval.workflowSha
|| identity.runId !== approval.runId || identity.attempt !== "1" || identity.root !== root) throw new Error("Private preflight identity changed");
const publisher = new BoundedHistoryPublisher(path.join(root, "preflight-requests.json"));
await verifyPrivateHistory(publisher, bucket);
const preflight = JSON.stringify({ ...identity, status: "verified", cellReportProducer: await inspectCellReportProducer() });
if (Buffer.byteLength(preflight) > 65536) throw new Error("Private preflight exceeds its byte allowance");
await writeFile(path.join(root, "preflight.json"), preflight, { mode: 0o600, flag: "wx" });
}
export async function validateSealedInventory(directory: string, inventory: string, id?: string, treeKind: "standard" | "generated-report" = "standard", context?: ManualPrivateHandoff) {
const cellOwner = treeKind === "standard" && context?.cellReportProducer
? await privateCellEvidenceOwner(context, id ?? (context.executionIds.length === 1 ? context.executionIds[0]! : "")) : undefined;
const receipt = JSON.parse(await readFile(path.join(directory, inventory), "utf8"));
if (receipt.protocol !== MANUAL_PRIVATE_PROTOCOL || id !== undefined && receipt.executionId !== id || receipt.complete !== true
|| !Array.isArray(receipt.files) || !receipt.files.length || receipt.files.length > 4096
|| hash(JSON.stringify(receipt.files)) !== receipt.inventorySha256) throw new Error("Original evidence inventory is incomplete");
if (id === undefined && receipt.treeKind !== treeKind) throw new Error("Private evidence tree kind differs from its inventory");
const expected = new Set<string>(inventory.startsWith("../") ? [] : [inventory]); let bytes = 0;
for (const row of receipt.files) {
if (!row || Object.keys(row).sort().join() !== "bytes,path,sha256" || typeof row.path !== "string"
|| row.path.split("/").some((part: string) => !part || part === "." || part === "..") || row.path.includes("\\")
|| expected.has(row.path) || !Number.isSafeInteger(row.bytes) || row.bytes < 0 || !/^[0-9a-f]{64}$/.test(row.sha256)) throw new Error("Invalid original evidence inventory row");
const cellAsset = cellOwner ? cellTraceAsset(row.path, cellOwner, id === undefined ? "final" : "original") : undefined;
if (cellAsset?.reserved && !cellAsset.canonical) throw new Error("Invalid private cell trace owner");
const standard = isStandardEvidenceFile(id === undefined ? `html-report/${row.path}` : row.path);
const canonical = id === undefined && treeKind === "generated-report" ? canonicalGeneratedReportAsset(row.path) : undefined;
const staticReportOwner = id === undefined && treeKind === "generated-report" && (row.path.startsWith("html/trace/") || canonical !== undefined);
if (cellAsset?.reserved ? !cellAsset.canonical : staticReportOwner ? !canonical : !standard) throw new Error("Invalid original evidence inventory row");
if (row.bytes > 256 * 1024 * 1024 - bytes) throw new Error("Original evidence byte accounting changed");
expected.add(row.path); bytes += row.bytes;
const file = path.join(directory, row.path), metadata = await lstat(file);
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size !== row.bytes) throw new Error("Original evidence changed after its scan");
const data = cellAsset?.reserved
? (await readPrivateCellTraceAsset({ owner: cellOwner!, layout: id === undefined ? "final" : "original", relative: row.path, file, remaining: row.bytes, secrets: [] }))!
: await readFile(file);
if (hash(data) !== row.sha256) throw new Error("Original evidence changed after its scan");
if (staticReportOwner && !await isTrustedGeneratedReportAsset(row.path, data)) throw new Error("Invalid original evidence inventory row");
}
if (bytes !== receipt.sourceBytes || bytes > 256 * 1024 * 1024) throw new Error("Original evidence byte accounting changed");
const actual = new Set<string>();
async function visit(relative: string) {
for (const entry of await readdir(path.join(directory, relative), { withFileTypes: true })) {
const name = relative ? `${relative}/${entry.name}` : entry.name;
if (entry.isSymbolicLink() || !entry.isFile() && !entry.isDirectory() || name.split("/").length > 24) throw new Error("Invalid sealed evidence entry");
if (entry.isDirectory()) { await visit(name); continue; }
if (!expected.has(name)) throw new Error("Unscanned file added to sealed evidence");
actual.add(name);
}
}
await visit("");
if (actual.size !== expected.size) throw new Error("Sealed evidence is incomplete");
if (treeKind === "standard") for (const row of receipt.files) {
if (row.path === PRIVATE_FRAMEWORK_REPORT || row.path.endsWith(`/${PRIVATE_FRAMEWORK_REPORT}`)) {
const cell = path.join(directory, row.path.slice(0, -PRIVATE_FRAMEWORK_REPORT.length));
if (id !== undefined) await validatePrivateFrameworkReport(cell);
else {
const metadata = JSON.parse(await readFile(path.join(directory, row.path), "utf8"));
const owner = metadata.canonicalExecutionId;
if (!context || typeof owner !== "string" || !context.executionIds.includes(owner)) throw new Error("Unbound canonical framework evidence");
await validatePrivateFrameworkReport(cell, path.join(context.root, "originals", manualCellKind(owner)), owner);
}
}
}
return receipt;
}
export async function validateSealedOriginals(root: string, id: string, context?: ManualPrivateHandoff) {
const receipt = await validateSealedInventory(path.join(root, "originals", manualCellKind(id)), "original-evidence-inventory.json", id, "standard", context);
const missing = receipt.missingRequiredEvidence;
const allowed = new Set(["final-state.png", "result.json", "junit.xml", "html-report/index.html", "snapshots/fixtures.json", "snapshots/api-state.json", "blob-report/*.zip"]);
if (![true, false, null].includes(receipt.requiredEvidenceComplete) || !Array.isArray(missing) || new Set(missing).size !== missing.length
|| missing.some((name: unknown) => typeof name !== "string" || !allowed.has(name))
|| (receipt.requiredEvidenceComplete === false) !== (missing.length > 0)) throw new Error("Required evidence completeness is unknown");
return receipt;
}
function cellCompleteness(id: string, receipt: { requiredEvidenceComplete: boolean | null; missingRequiredEvidence: string[] }): PrivateEvidenceCompleteness {
return { meaning: "available-files-and-transfer-only", doesNotAttestFeatureOutcome: true,
cells: [{ executionId: id, availableFileInventoryComplete: true, requiredPassEvidenceComplete: receipt.requiredEvidenceComplete, missingRequiredEvidence: receipt.missingRequiredEvidence }] };
}
async function uploadCell(env: NodeJS.ProcessEnv) {
requireSession(env); const { approval, bucket } = request(env), id = env.SELECTED_EXECUTION_ID!;
if (!approval.executionIds.includes(id)) throw new Error("Private evidence cell is not approved");
const root = await realpath(env.PRIVATE_EVIDENCE_ROOT ?? ""), context = JSON.parse(await readFile(path.join(root, "preflight.json"), "utf8"));
if (context.status !== "verified" || context.approvalSha256 !== approval.approvalSha256 || context.targetSha !== approval.targetSha
|| context.runId !== approval.runId || context.executionIds.join() !== id || context.root !== root) throw new Error("Private evidence preflight identity changed");
if (await lstat(path.join(root, "contamination.json")).then(() => true, error => { if (error.code === "ENOENT") return false; throw error; }))
throw new Error("Contaminated private evidence cannot be uploaded");
const originals = await validateSealedOriginals(root, id, context);
const evidenceCompleteness = cellCompleteness(id, originals);
const bundle = path.join(root, "sealed-bundle"); await mkdir(bundle, { mode: 0o700 });
const results = path.join(root, "sealed-results");
await validateSealedInventory(path.join(results, "html-report"), "../result-evidence-inventory.json", undefined, "standard", context);
await mkdir(path.join(bundle, "originals"), { mode: 0o700 });
await cp(path.join(root, "originals", manualCellKind(id)), path.join(bundle, "originals", manualCellKind(id)), { recursive: true, errorOnExist: true, force: false });
await cp(results, path.join(bundle, "sealed-results"), { recursive: true, errorOnExist: true, force: false });
await writeFile(path.join(bundle, "identity.json"), JSON.stringify({ ...context, evidenceCompleteness }), { mode: 0o600, flag: "wx" });
const receipt = path.join(root, "cell-transfer.json"), publisher = new BoundedHistoryPublisher(receipt);
await retainPrivateTransfer({ operation: "upload", kind: manualCellKind(id), runId: approval.runId, directory: bundle, bucket, publisher, evidenceCompleteness }, receipt,
() => new BoundedHistoryPublisher(`${receipt}.retention.json`));
}
async function reportSession(env: NodeJS.ProcessEnv) {
const { approval, bucket } = request(env);
await output("session_policy", JSON.stringify(manualPrivateSessionPolicy({ bucket, runId: approval.runId, executionIds: [...approval.executionIds], mode: "report-write" })));
await output("cell_count", String(approval.executionIds.length));
for (const [index, id] of approval.executionIds.entries()) {
await output(`read_policy_${index}`, JSON.stringify(manualPrivateSessionPolicy({ bucket, runId: approval.runId, executionIds: [id], mode: "cell-read" })));
}
}
async function downloadCell(env: NodeJS.ProcessEnv) {
requireSession(env); const { approval, bucket } = request(env), root = await realpath(env.PRIVATE_REPORT_ROOT ?? "");
const index = Number(env.SELECTED_CELL_INDEX);
if (!/^[0-7]$/.test(env.SELECTED_CELL_INDEX ?? "") || !approval.executionIds[index]) throw new Error("Invalid exact private cell index");
const selected = path.join(root, "selected-runner-e2e"); await mkdir(selected, { recursive: true, mode: 0o700 });
const id = approval.executionIds[index]!;
{
const directory = path.join(root, manualCellKind(id)), publisher = new BoundedHistoryPublisher(path.join(root, `${manualCellKind(id)}-download.json`));
const receipt = await publisher.downloadJson<{ evidenceCompleteness?: PrivateEvidenceCompleteness; transfer?: { status?: string; sha256?: string } }>(bucket,
`${privateArtifactKey(manualCellKind(id), approval.runId)}.upload-receipt.json`, path.join(root, `${manualCellKind(id)}-receipt.json`));
if (receipt?.transfer?.status !== "complete" || !/^[0-9a-f]{64}$/.test(receipt.transfer.sha256 ?? "")) throw new Error("Private cell transfer receipt is unavailable");
const downloaded = await transferPrivateArtifact({ operation: "download", kind: manualCellKind(id), runId: approval.runId, directory, bucket, publisher });
if (downloaded.sha256 !== receipt.transfer.sha256) throw new Error("Private cell archive differs from retained transfer receipt");
const identity = JSON.parse(await readFile(path.join(directory, "identity.json"), "utf8"));
if (identity.targetSha !== approval.targetSha || identity.workflowSha !== approval.workflowSha || identity.runId !== approval.runId
|| identity.executionIds.join() !== id || identity.protocol !== MANUAL_PRIVATE_PROTOCOL) throw new Error("Private cell archive has a foreign identity");
const originals = await validateSealedOriginals(directory, id, { ...identity, root: directory }), summary = cellCompleteness(id, originals);
if (hash(JSON.stringify(identity.evidenceCompleteness)) !== hash(JSON.stringify(summary))
|| hash(JSON.stringify(receipt.evidenceCompleteness)) !== hash(JSON.stringify(summary))) throw new Error("Private evidence completeness receipt differs from archive");
await stageSealedPrivateCell(directory, selected, id, { ...identity, root: directory });
await writeFile(path.join(root, `manual-cell-completeness-${index}.json`), JSON.stringify(summary), { mode: 0o600, flag: "wx" });
}
}
export async function stageSealedPrivateCell(directory: string, selected: string, id: string, context: ManualPrivateHandoff) {
await validateSealedOriginals(directory, id, context);
const inventory = await validateSealedInventory(path.join(directory, "sealed-results", "html-report"), "../result-evidence-inventory.json", undefined, "standard", context);
const destination = path.join(selected, manualCellKind(id));
await cp(path.join(directory, "sealed-results", "html-report"), destination, { recursive: true, errorOnExist: true, force: false });
for (const row of inventory.files) {
if (row.path === PRIVATE_FRAMEWORK_REPORT || row.path.endsWith(`/${PRIVATE_FRAMEWORK_REPORT}`)) {
const cell = path.join(destination, row.path.slice(0, -PRIVATE_FRAMEWORK_REPORT.length));
await materializePrivateFrameworkAttachments(cell, path.join(directory, "originals", manualCellKind(id)), id);
}
}
}
async function verifyDownloads(env: NodeJS.ProcessEnv) {
const { approval } = request(env), root = await realpath(env.PRIVATE_REPORT_ROOT ?? "");
const cells: PrivateEvidenceCompleteness["cells"] = [];
for (const [index, id] of approval.executionIds.entries()) {
const summary = JSON.parse(await readFile(path.join(root, `manual-cell-completeness-${index}.json`), "utf8")) as PrivateEvidenceCompleteness;
if (summary.meaning !== "available-files-and-transfer-only" || summary.doesNotAttestFeatureOutcome !== true
|| summary.cells.length !== 1 || summary.cells[0]!.executionId !== id) throw new Error("Private report lacks an exact cell transfer");
cells.push(...summary.cells);
}
await writeFile(path.join(root, "manual-report-input-completeness.json"), JSON.stringify({ meaning: "available-files-and-transfer-only", doesNotAttestFeatureOutcome: true, cells }), { mode: 0o600, flag: "wx" });
}
export async function uploadReport(env: NodeJS.ProcessEnv) {
requireSession(env); const { approval, bucket } = request(env), source = await realpath(env.PRIVATE_REPORT_SOURCE ?? ""), root = await realpath(env.PRIVATE_REPORT_ROOT ?? "");
if (!source.startsWith(`${root}${path.sep}`)) throw new Error("Private report escaped owned staging");
const evidenceCompleteness = JSON.parse(await readFile(path.join(root, "manual-report-input-completeness.json"), "utf8")) as PrivateEvidenceCompleteness;
if (evidenceCompleteness.meaning !== "available-files-and-transfer-only" || evidenceCompleteness.doesNotAttestFeatureOutcome !== true
|| evidenceCompleteness.cells.map(cell => cell.executionId).sort().join() !== [...approval.executionIds].sort().join()) throw new Error("Private report lacks exact cell completeness receipts");
const secrets = normalizedSecrets([...CREDENTIAL_NAMES.map(name => env[name]), env.AWS_ACCESS_KEY_ID, env.AWS_SECRET_ACCESS_KEY, env.AWS_SESSION_TOKEN]);
await sealManualResultEvidence({ root, protocol: MANUAL_PRIVATE_PROTOCOL, private: true, workflowSha: approval.workflowSha,
targetSha: approval.targetSha, runId: approval.runId, attempt: approval.attempt, executionIds: [...approval.executionIds] }, secrets, source, "generated-report");
const sealed = path.join(root, "sealed-results");
await validateSealedInventory(path.join(sealed, "html-report"), "../result-evidence-inventory.json", undefined, "generated-report");
const receipt = path.join(root, "report-transfer.json"), publisher = new BoundedHistoryPublisher(receipt);
await retainPrivateTransfer({ operation: "upload", kind: "manual-report", runId: approval.runId, directory: sealed, bucket, publisher, evidenceCompleteness }, receipt,
() => new BoundedHistoryPublisher(`${receipt}.retention.json`));
}
async function main() {
const action = process.argv[2];
if (process.argv.length !== 3) throw new Error("Invalid private evidence command");
if (action === "prepare") await prepare(process.env);
else if (action === "verify") await verify(process.env);
else if (action === "upload-cell") await uploadCell(process.env);
else if (action === "report-session") await reportSession(process.env);
else if (action === "download-cell") await downloadCell(process.env);
else if (action === "verify-downloads") await verifyDownloads(process.env);
else if (action === "upload-report") await uploadReport(process.env);
else throw new Error("Invalid private evidence command");
}
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
main().catch(() => { process.stderr.write("Restricted manual evidence operation failed; no public fallback\n"); process.exitCode = 1; });
}