mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 17:33:51 +02:00
193 lines
16 KiB
TypeScript
193 lines
16 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, without an added shell prefix or suffix:\n${c.validationCommand}\nIt independently reads the file, fails on incorrect bytes, and prints the validation marker only after success.`,
|
|
`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 Verified text, and a fresh idempotencyKey. 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. */
|
|
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)
|
|
&& 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");
|
|
function lifecycle(end: typeof edit) {
|
|
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 && x.p.name === end.p.name && x.p.operation === end.p.operation
|
|
&& x.p.target === end.p.target && 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")), "incomplete or unsuccessful native tool lifecycle");
|
|
return starts[0]!;
|
|
}
|
|
lifecycle(edit);
|
|
const validations = tools.filter(x => x.e.eventType === "tool.execution.completed" && x.p.operation === "execute"
|
|
&& x.p.name === c.validationCommand && typeof x.p.output === "string" && x.p.output.includes(c.validationMarker));
|
|
requireEvidence(validations.length === 1, "one observed provider validation execution");
|
|
const validation = validations[0]!, start = lifecycle(validation);
|
|
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");
|
|
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,
|
|
commandTitle: validation.p.name, 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: ["Validation command title is checked exactly; raw arguments and typed exitCode are not projected, so raw invocation/exit code are not attested.",
|
|
"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 });
|
|
}
|