test: require exact attested bootstrap reads in native qualification

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-29 17:54:12 -05:00
1 parent b41ff0377b
commit 794e90a258
12 files changed
+278 -33

No files matched your search

+11
View File
@@ -336,3 +336,14 @@ retirement checks. Runtime-internal files are an explicit scoped exclusion.
`human-permission-denial` requires native request/tool correlation, the browser's
exact Decline, delivered denial and failed write, and unchanged file evidence
through retirement. All normal project and company boundaries apply.
Cursor and Copilot may exempt a bootstrap read only when every native notice for
that completed tool origin carries the single-path attestation matching the
observer's exact random action file. The passive projector derives this digest
from one explicit scalar native input path, rejects ambiguous/multiple paths,
and checks subsequent input/location updates for changes. Durable canonical
execution receipts must match the same run, turn, execution identity, status,
and order; any explicit canonical target must agree. Missing or conflicting
attestations fail qualification. PRP retains only the first location and terminal
updates may omit it, so canonical targets alone cannot prove bootstrap ownership.
Neither tool titles nor output text supplies path evidence.
+3 -2
View File
@@ -6,13 +6,13 @@ export interface CopilotToolNotice {
status?: "pending" | "in_progress" | "completed" | "failed";
operation?: "edit" | "execute" | "read"; target?: string; requestId?: string;
declineOffered?: boolean; outcome?: "allow_once" | "allow_always" | "reject_once" | "cancel";
commandSha256?: string; mode?: "sync" | "async"; detach?: boolean;
commandSha256?: string; readTargetSha256?: string; mode?: "sync" | "async"; detach?: boolean;
shellId?: string; commandToolCallId?: string; shellState?: "started" | "completed"; exitCode?: number;
}
const rec = (v: unknown): Record<string, any> => v !== null && typeof v === "object" && !Array.isArray(v) ? v as Record<string, any> : {};
const id = (v: unknown): v is string => typeof v === "string" && v.length > 0 && v.length <= 240 && !/[\u0000-\u001f\u007f]/u.test(v) && !v.includes("[REDACTED]");
const enums = { stage: ["tool", "permission_requested", "permission_delivered"], status: ["pending", "in_progress", "completed", "failed"], operation: ["edit", "execute", "read"], outcome: ["allow_once", "allow_always", "reject_once", "cancel"], mode: ["sync", "async"], shellState: ["started", "completed"] };
const names = new Set(["stage", "toolCallId", "status", "operation", "target", "requestId", "declineOffered", "outcome", "commandSha256", "mode", "detach", "shellId", "commandToolCallId", "shellState", "exitCode"]);
const names = new Set(["stage", "toolCallId", "status", "operation", "target", "requestId", "declineOffered", "outcome", "commandSha256", "readTargetSha256", "mode", "detach", "shellId", "commandToolCallId", "shellState", "exitCode"]);
export function readCopilotToolEvidence(rows: readonly unknown[], expectedRunId: string): CopilotToolNotice[] {
const result: CopilotToolNotice[] = [];
for (const row of rows) {
@@ -38,6 +38,7 @@ export function readCopilotToolEvidence(rows: readonly unknown[], expectedRunId:
if (!fields.target || fields.target.startsWith("/") || /[\\:\u0000-\u001f\u007f]/u.test(fields.target) || fields.target.split("/").some(x => x === "." || x === ".." || !x)) throw new Error("Invalid Copilot evidence target");
notice.target = fields.target;
}
if (fields.readTargetSha256 !== undefined) { if (!/^sha256:[a-f0-9]{64}$/u.test(fields.readTargetSha256)) throw new Error("Invalid read digest"); notice.readTargetSha256 = fields.readTargetSha256; }
if (fields.commandSha256 !== undefined) { if (!/^sha256:[a-f0-9]{64}$/u.test(fields.commandSha256)) throw new Error("Invalid command digest"); notice.commandSha256 = fields.commandSha256; }
if (fields.shellId !== undefined) { if (!/^[A-Za-z0-9_.-]{1,80}$/u.test(fields.shellId)) throw new Error("Invalid shell identity"); notice.shellId = fields.shellId; }
if (fields.exitCode !== undefined) { if (!/^-?\d{1,10}$/u.test(fields.exitCode) || !Number.isSafeInteger(Number(fields.exitCode))) throw new Error("Invalid exit code"); notice.exitCode = Number(fields.exitCode); }
@@ -1,3 +1,4 @@
import { createHash } from "node:crypto";
import { mkdtemp, readFile, rm, unlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
@@ -69,14 +70,25 @@ describe("Copilot sealed remote proof", () => {
]) { const s = receipt(); mutate(s); expect(() => assertCopilotRemoteAttached(s, baseline(), 50)).toThrow(); }
expect(() => assertCopilotRemoteAttached(receipt(), baseline(), 20)).toThrow();
});
it("allows only explicit same-turn bootstrap reads before the tested native origin", () => {
it("exempts only exact completed action-file reads before the tested operation", () => {
const call = { ...notice, seq: 10, status: "in_progress" as const };
const read = { ...notice, toolCallId: "bootstrap", operation: "read", seq: 1 } as unknown as CopilotToolNotice;
expect(countCopilotToolOrigins(copilotActionNotices([read, call], call, true))).toBe(1);
for (const bad of [{ ...read, seq: 11 }, { ...read, turnId: "other" }, { ...read, operation: undefined }, { ...read, operation: "edit" as const }]) {
expect(countCopilotToolOrigins(copilotActionNotices([bad, call], call, true))).toBe(2);
const read = { ...notice, toolCallId: "bootstrap", operation: "read", status: "completed", seq: 1 } as CopilotToolNotice;
const actionFile = `.paperclip-eval-action-${"a".repeat(36)}.txt`;
const event = { runId: "run", seq: 2, protocolSchemaVersion: 1, eventType: "tool.execution.completed", payload: { prpEvent: {
schema: "paperclip.prp.event.v1", schemaVersion: 1, sourceKind: "runner", runId: "run", turnId: "turn", eventType: "tool.execution.completed", emittedAt: new Date(2).toISOString(),
payload: { schema: "paperclip.tool.execution.v1", executionId: "bootstrap", transport: "builtin", operation: "read", target: actionFile, status: "completed" },
} } };
read.readTargetSha256 = `sha256:${createHash("sha256").update(actionFile).digest("hex")}`;
const proof = { actionFile, events: [event] };
expect(countCopilotToolOrigins(copilotActionNotices([read, call], call, proof))).toBe(1);
const wrong = structuredClone(event); wrong.payload.prpEvent.payload.target = "unrelated.txt";
expect(() => copilotActionNotices([read, call], call, { actionFile, events: [wrong] })).toThrow();
expect(() => copilotActionNotices([read, { ...read, toolCallId: "unrelated", seq: 3 }, call], call, proof)).toThrow();
expect(() => copilotActionNotices([{ ...read, turnId: "other" }, call], call, proof)).toThrow();
for (const bad of [{ ...read, seq: 11 }, { ...read, operation: undefined }, { ...read, operation: "edit" as const }]) {
expect(countCopilotToolOrigins(copilotActionNotices([bad, call], call, { actionFile, events: [] }))).toBe(2);
}
expect(countCopilotToolOrigins(copilotActionNotices([read, call], call, false))).toBe(2);
expect(countCopilotToolOrigins(copilotActionNotices([read, call], call))).toBe(2);
});
it("awaits the remote fixture and baseline before disclosing the actual command", async () => {
const s = baseline(), order: string[] = [];
@@ -1,3 +1,4 @@
import { withoutProvenBootstrapReads, type BootstrapReadProof } from "./native-bootstrap-read-proof.js";
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import type { CopilotToolNotice } from "./copilot-evidence.js";
@@ -66,9 +67,8 @@ export function copilotRemoteDeniedSample(s: CopilotRemoteSnapshot, baseline: Co
return { phase, observedAtMs: s.observedAtMs, exists: t.absent !== true || t.sha256 !== null };
}
/** Only typed reads before the tested operation can be bootstrap work. */
export function copilotActionNotices(notices: readonly CopilotToolNotice[], origin: CopilotToolNotice, remote: boolean): CopilotToolNotice[] {
return notices.filter(n => !(remote && (n.operation as string) === "read" && n.seq < origin.seq
&& n.runId === origin.runId && n.sessionId === origin.sessionId && n.turnId === origin.turnId));
export function copilotActionNotices(notices: readonly CopilotToolNotice[], origin: CopilotToolNotice, proof?: BootstrapReadProof): CopilotToolNotice[] {
return withoutProvenBootstrapReads(notices, origin, proof);
}
export function assertCopilotRemoteAttached(s: CopilotRemoteSnapshot, baseline: CopilotRemoteSnapshot, terminalAt: number) {
assertCopilotRemoteRetirement(s, baseline);
+2 -2
View File
@@ -152,12 +152,12 @@ export async function runCopilotProtectionFlow(input: {
await input.evidence("copilot-denial-proof.json", { evidence, processes, notices });
const grade = gradeCopilotDeniedWrite(evidence); check("denial-without-side-effects", grade.passed, grade.failures.join(", ") || "Exact browser denial, explicit cancellation and absence through process cleanup");
check("negative-task-unfinished", issue.status === "in_progress", "The negative test does not claim the task is done");
check("no-extra-native-operation", countCopilotToolOrigins(copilotActionNotices(notices, notices.find(n => n.stage === "tool" && n.toolCallId === request.toolCallId)!, remote)) === 1, "No alternate native edit, command or delegated operation is permitted");
check("no-extra-native-operation", countCopilotToolOrigins(copilotActionNotices(notices, notices.find(n => n.stage === "tool" && n.toolCallId === request.toolCallId)!, remoteFixture ? { actionFile: remoteFixture.actionFile, events: runEvents } : undefined)) === 1, "No alternate native edit, command or delegated operation is permitted");
} else {
await wait("attached command and task settlement", s => s.issue.status === "done" && s.runs[0]?.status === "succeeded" && (remote || (s.processes.captured && s.processes.live.length === 0)));
const call = notices.find(n => n.stage === "tool" && n.status === "pending" && n.commandSha256 === exactCommand()!.commandSha256);
check("single-exact-command", Boolean(call) && countCopilotToolOrigins(notices.filter(n => n.commandSha256 === exactCommand()!.commandSha256)) === 1, "Exactly one native execution contains the supplied command digest");
if (remote) check("no-extra-native-operation", copilotActionNotices(notices, call!, true).every(n =>
if (remote) check("no-extra-native-operation", copilotActionNotices(notices, call!, { actionFile: remoteFixture!.actionFile, events: runEvents }).every(n =>
n.runId === call!.runId && n.sessionId === call!.sessionId && n.turnId === call!.turnId
&& (n.toolCallId === call!.toolCallId || n.commandToolCallId === call!.toolCallId)), "Only setup reads and the exact native attached command/result are allowed");
const started = notices.find(n => n.toolCallId === call!.toolCallId && n.shellState === "started");
+3 -2
View File
@@ -1,3 +1,4 @@
import type { BootstrapReadProof } from "./native-bootstrap-read-proof.js";
import { cursorDeniedCommand, hasCursorDeniedCommand, type CursorToolNotice } from "./cursor-native-evidence.js";
import type { RunnerTaskFixture } from "./types.js";
@@ -81,14 +82,14 @@ export const CURSOR_DENIAL_SAMPLE_PHASES = ["before-request", "pending", "browse
/** Samples are taken independently by the fixture, never supplied by the model. */
export function hasCursorDenialBoundary(input: {
request: unknown; expectedRequestId: string; expectedToolCallId: string;
path: string; allowBootstrapReads?: boolean; notices: readonly CursorToolNotice[]; runId: string; turnId: string; samples: readonly { phase: string; path: string; absent: boolean; observedAt: number }[];
path: string; bootstrapReadProof?: BootstrapReadProof; notices: readonly CursorToolNotice[]; runId: string; turnId: string; samples: readonly { phase: string; path: string; absent: boolean; observedAt: number }[];
}): boolean {
const request = record(input.request); const origin = record(request.origin); const details = record(request.details);
if (!input.expectedRequestId.trim() || !input.expectedToolCallId.trim() || !input.path.trim()
|| request.requestId !== input.expectedRequestId || details.toolCallId !== input.expectedToolCallId
|| request.type !== "permission" || request.status !== "pending"
|| origin.provider !== "cursor" || origin.adapter !== "acpx-runtime" || origin.method !== "session/request_permission") return false;
if (request.turnId !== input.turnId || !hasCursorDeniedCommand({ notices: input.notices, runId: input.runId, turnId: input.turnId, requestId: input.expectedRequestId, toolCallId: input.expectedToolCallId, commandSha256: cursorDeniedCommand(input.path).commandSha256, allowBootstrapReads: input.allowBootstrapReads })) return false;
if (request.turnId !== input.turnId || !hasCursorDeniedCommand({ notices: input.notices, runId: input.runId, turnId: input.turnId, requestId: input.expectedRequestId, toolCallId: input.expectedToolCallId, commandSha256: cursorDeniedCommand(input.path).commandSha256, bootstrapReadProof: input.bootstrapReadProof })) return false;
const choices = request.choices;
if (!Array.isArray(choices)) return false;
const keys = choices.map(value => record(value).key);
@@ -1,3 +1,4 @@
import { createHash } from "node:crypto";
import { expect, it } from "vitest";
import { cursorDeniedCommand, hasCursorDeniedCommand, hasCursorCancellation, readCursorToolEvidence, type CursorToolNotice } from "./cursor-native-evidence.js";
@@ -85,8 +86,20 @@ it("allows explicit earlier remote bootstrap reads while rejecting unknown kinds
const read: CursorToolNotice = { ...rows[0]!, seq: 1, toolCallId: "bootstrap-read", stage: "tool", status: "completed", operation: "read", commandSha256: undefined };
const input = { notices: [read, ...rows], runId: "run", turnId: "turn", requestId: "request", toolCallId: "tool", commandSha256: cursorDeniedCommand("/fixture/denied.txt").commandSha256 };
expect(hasCursorDeniedCommand(input)).toBe(false);
expect(hasCursorDeniedCommand({ ...input, allowBootstrapReads: true })).toBe(true);
for (const operation of [undefined, "edit", "execute"]) expect(hasCursorDeniedCommand({ ...input, allowBootstrapReads: true, notices: [{ ...read, operation }, ...rows] })).toBe(false);
expect(hasCursorDeniedCommand({ ...input, allowBootstrapReads: true, notices: [...rows, { ...read, seq: 20 }] })).toBe(false);
expect(hasCursorDeniedCommand({ ...input, allowBootstrapReads: true, notices: [{ ...read, sessionId: "other" }, ...rows] })).toBe(false);
const actionFile = `.paperclip-eval-action-${"a".repeat(36)}.txt`;
const event = { runId: "run", seq: 2, protocolSchemaVersion: 1, eventType: "tool.execution.completed", payload: { prpEvent: {
schema: "paperclip.prp.event.v1", schemaVersion: 1, sourceKind: "runner", runId: "run", turnId: "turn", eventType: "tool.execution.completed", emittedAt: new Date(2).toISOString(),
payload: { schema: "paperclip.tool.execution.v1", executionId: "bootstrap-read", transport: "builtin", operation: "read", target: actionFile, status: "completed" },
} } };
read.readTargetSha256 = `sha256:${createHash("sha256").update(actionFile).digest("hex")}`;
const bootstrapReadProof = { actionFile, events: [event] };
expect(hasCursorDeniedCommand({ ...input, bootstrapReadProof })).toBe(true);
expect(hasCursorDeniedCommand({ ...input, bootstrapReadProof: { actionFile, events: [] } })).toBe(false);
const wrong = structuredClone(event); wrong.payload.prpEvent.payload.target = "unrelated.txt";
expect(hasCursorDeniedCommand({ ...input, bootstrapReadProof: { actionFile, events: [wrong] } })).toBe(false);
const extra = { ...read, toolCallId: "unrelated", seq: 0 };
expect(hasCursorDeniedCommand({ ...input, notices: [extra, ...input.notices], bootstrapReadProof })).toBe(false);
for (const operation of [undefined, "edit", "execute"]) expect(hasCursorDeniedCommand({ ...input, bootstrapReadProof, notices: [{ ...read, operation }, ...rows] })).toBe(false);
expect(hasCursorDeniedCommand({ ...input, bootstrapReadProof, notices: [...rows, { ...read, seq: 20 }] })).toBe(false);
expect(hasCursorDeniedCommand({ ...input, bootstrapReadProof, notices: [{ ...read, sessionId: "other" }, ...rows] })).toBe(false);
});
+8 -7
View File
@@ -1,15 +1,16 @@
import { withoutProvenBootstrapReads, type BootstrapReadProof } from "./native-bootstrap-read-proof.js";
import { createHash } from "node:crypto";
import { isAbsolute, posix } from "node:path";
export interface CursorToolNotice {
runId: string; sessionId: string; turnId: string; toolCallId: string; seq: number;
stage: "tool" | "permission_requested" | "permission_delivered";
status?: string; operation?: string; commandSha256?: string; requestId?: string; declineOffered?: boolean; outcome?: string;
status?: string; operation?: string; commandSha256?: string; readTargetSha256?: string; requestId?: string; declineOffered?: boolean; outcome?: string;
}
const rec = (v: unknown): Record<string, any> => v !== null && typeof v === "object" && !Array.isArray(v) ? v as Record<string, any> : {};
const id = (v: unknown): v is string => typeof v === "string" && v.length > 0 && v.length <= 240 && !/[\u0000-\u001f\u007f]/u.test(v) && !v.includes("[REDACTED]");
const enums = { stage: ["tool", "permission_requested", "permission_delivered"], status: ["pending", "in_progress", "completed", "failed"], operation: ["execute", "read"], outcome: ["allow_once", "allow_always", "reject_once", "cancel"] };
const names = new Set(["stage", "toolCallId", "status", "operation", "commandSha256", "requestId", "declineOffered", "outcome"]);
const names = new Set(["stage", "toolCallId", "status", "operation", "commandSha256", "readTargetSha256", "requestId", "declineOffered", "outcome"]);
export function readCursorToolEvidence(rows: readonly unknown[], runId: string): CursorToolNotice[] {
const result: CursorToolNotice[] = [];
for (const value of rows) {
@@ -27,6 +28,7 @@ export function readCursorToolEvidence(rows: readonly unknown[], runId: string):
if (!id(fields.toolCallId) || !enums.stage.includes(fields.stage!) || origin.eventType !== fields.stage || origin.method !== (fields.stage === "tool" ? "session/update" : "session/request_permission")) throw new Error("Invalid Cursor evidence origin");
for (const [key, values] of Object.entries(enums)) if (fields[key] !== undefined && !values.includes(fields[key]!)) throw new Error("Invalid Cursor evidence enum");
if (fields.requestId !== undefined && !id(fields.requestId)) throw new Error("Invalid Cursor request identity");
if (fields.readTargetSha256 !== undefined && !/^sha256:[a-f0-9]{64}$/u.test(fields.readTargetSha256)) throw new Error("Invalid Cursor read digest");
if (fields.commandSha256 !== undefined && !/^sha256:[a-f0-9]{64}$/u.test(fields.commandSha256)) throw new Error("Invalid Cursor command digest");
if (fields.declineOffered !== undefined && !["true", "false"].includes(fields.declineOffered)) throw new Error("Invalid Cursor offered choice");
result.push({ ...fields, runId, sessionId: origin.sessionId, turnId: origin.turnId, seq: row.seq, declineOffered: fields.declineOffered === "true" } as CursorToolNotice);
@@ -42,13 +44,12 @@ export function cursorDeniedCommand(path: string) {
return { command, commandSha256: `sha256:${createHash("sha256").update(command).digest("hex")}` };
}
export function hasCursorDeniedCommand(input: {
notices: readonly CursorToolNotice[]; runId: string; turnId: string; requestId: string; toolCallId: string; commandSha256: string; allowBootstrapReads?: boolean;
notices: readonly CursorToolNotice[]; runId: string; turnId: string; requestId: string; toolCallId: string; commandSha256: string; bootstrapReadProof?: BootstrapReadProof;
}): boolean {
const origin = input.notices.find(row => row.stage === "tool" && row.status === "pending" && row.operation === "execute" && row.toolCallId === input.toolCallId && row.commandSha256 === input.commandSha256);
// Only explicit native reads before the tested command may belong to the
// remote action-file bootstrap. Undefined kinds or later work remain failures.
const notices = input.notices.filter(row => !(input.allowBootstrapReads && origin && row.operation === "read" && row.seq < origin.seq
&& row.runId === input.runId && row.turnId === input.turnId && row.sessionId === origin.sessionId));
let notices: readonly CursorToolNotice[];
try { notices = origin ? withoutProvenBootstrapReads(input.notices, origin, input.bootstrapReadProof) : input.notices; }
catch { return false; }
const requests = notices.filter(row => row.stage === "permission_requested");
if (requests.length !== 1) return false;
const request = requests[0]!;
+1 -1
View File
@@ -39,7 +39,7 @@ it("awaits owned remote baseline before returning the exact remote denial comman
const binding = { companyId: "company", environmentId: "environment", runId: "run", leaseId: "lease", sandboxId: "sandbox", remoteCwd: "/home/daytona/paperclip-workspace", image: `runner@sha256:${"a".repeat(64)}` };
const snapshot = { binding, observedAtMs: 50, complete: true, workspace: {}, targets: { "denied.txt": { absent: true, sha256: null, parent: { dev: "1", ino: "2" }, mutationCount: 0, complete: true } }, watcher: { complete: true, targetMutationCount: 0, workspaceMutationCount: 0 }, processes: { captured: true, root: { pid: 51, startTicks: "3021", bootId: "12345678-1234-1234-1234-123456789abc" }, journal: [], live: [51] } };
const order: string[] = [];
const fixture = { binding, remoteCwd: binding.remoteCwd, snapshot: async (label: string) => { order.push(label); await Promise.resolve(); order.push("baseline-ready"); return snapshot; }, finish: async () => snapshot, close: async () => {} };
const fixture = { binding, actionFile: `.paperclip-eval-action-${"a".repeat(36)}.txt`, remoteCwd: binding.remoteCwd, snapshot: async (label: string) => { order.push(label); await Promise.resolve(); order.push("baseline-ready"); return snapshot; }, finish: async () => snapshot, close: async () => {} };
const prepared = await prepareCursorRemoteAction({ fixture, ...binding, deniedRelative: "denied.txt", prompt: "Perform the actual test" });
order.push("publish-action");
expect(order).toEqual(["before-action-publication", "baseline-ready", "publish-action"]);
+6 -6
View File
@@ -15,7 +15,7 @@ type Row = Record<string, any>;
type Check = { id: string; passed: boolean; detail: string };
export interface CursorRemoteNativeFixture {
binding: CursorRemoteBinding; remoteCwd: string;
binding: CursorRemoteBinding; remoteCwd: string; actionFile: string;
snapshot(label: string): Promise<CursorRemoteSnapshot>;
finish(): Promise<CursorRemoteSnapshot>;
close(): Promise<void>;
@@ -113,7 +113,7 @@ export async function runCursorNativeFlow(input: {
const deniedRelative = `cursor-denied-${nonce}.txt`;
let deniedPath = remote ? "" : join(input.workspacePath, deniedRelative);
let deniedCommand = remote ? null : cursorDeniedCommand(deniedPath);
let denialNotices: CursorToolNotice[] = []; let denialTurnId = ""; let cancelRequestedAt = NaN; let cancellationProven = false;
let denialNotices: CursorToolNotice[] = []; let denialRunEvents: Row[] = []; let denialTurnId = ""; let cancelRequestedAt = NaN; let cancellationProven = false;
const samples: Array<{ phase: string; path: string; absent: boolean; observedAt: number }> = [];
const sampleDenied = async (phase: string, retained?: CursorRemoteSnapshot) => {
let sample: { phase: string; path: string; absent: boolean; observedAt: number };
@@ -157,7 +157,7 @@ export async function runCursorNativeFlow(input: {
finalCheck("denied-absent-after-cleanup", finalSample.absent, "Independent target remains absent after observed provider retirement");
const journal = watch.finish();
finalCheck("continuous-denial-observation", journal.complete && journal.targetMutationCount === 0, "Continuous target watcher observed no create/delete mutation and retained directory identity");
finalCheck("complete-native-denial-boundary", Boolean(deniedRequest) && hasCursorDenialBoundary({ request: deniedRequest, expectedRequestId: deniedRequest?.requestId ?? "", expectedToolCallId: deniedRequest?.details.toolCallId ?? "", path: deniedPath, allowBootstrapReads: remote, samples, notices: denialNotices, runId: runs[0]?.id ?? "", turnId: denialTurnId }), "Supported native denial preserved the target through all six independent boundaries");
finalCheck("complete-native-denial-boundary", Boolean(deniedRequest) && hasCursorDenialBoundary({ request: deniedRequest, expectedRequestId: deniedRequest?.requestId ?? "", expectedToolCallId: deniedRequest?.details.toolCallId ?? "", path: deniedPath, bootstrapReadProof: remoteFixture ? { actionFile: remoteFixture.actionFile, events: denialRunEvents } : undefined, samples, notices: denialNotices, runId: runs[0]?.id ?? "", turnId: denialTurnId }), "Supported native denial preserved the target through all six independent boundaries");
await input.evidence("cursor-native-denial-final.json", { request: deniedRequest, samples, notices: denialNotices, commandSha256: deniedCommand!.commandSha256, cancellationProven, cancelRequestedAt, processes, processObservationError, watcher: journal, checks: cleanupChecks });
if (cleanupChecks.some(row => !row.passed)) throw new Error("Cursor native denial cleanup proof is incomplete or observed an effect");
return cleanupChecks;
@@ -178,7 +178,7 @@ export async function runCursorNativeFlow(input: {
if (design.id === "native-write-deny-reconnect") {
await sampleDenied("after-cleanup", snapshot);
finalCheck("remote-cancel-terminal", cancellationProven && runs[0]?.status === "cancelled" && issue.status === "in_progress", "Explicit native cancellation remains durable without false task completion");
finalCheck("remote-no-denied-effect", snapshot.watcher.targetMutationCount === 0 && Boolean(deniedRequest) && hasCursorDenialBoundary({ request: deniedRequest, expectedRequestId: deniedRequest?.requestId ?? "", expectedToolCallId: deniedRequest?.details.toolCallId ?? "", path: deniedPath, allowBootstrapReads: remote, samples, notices: denialNotices, runId: runs[0]?.id ?? "", turnId: denialTurnId }), "Exact denied native command caused no remote file effect through provider retirement");
finalCheck("remote-no-denied-effect", snapshot.watcher.targetMutationCount === 0 && Boolean(deniedRequest) && hasCursorDenialBoundary({ request: deniedRequest, expectedRequestId: deniedRequest?.requestId ?? "", expectedToolCallId: deniedRequest?.details.toolCallId ?? "", path: deniedPath, bootstrapReadProof: remoteFixture ? { actionFile: remoteFixture.actionFile, events: denialRunEvents } : undefined, samples, notices: denialNotices, runId: runs[0]?.id ?? "", turnId: denialTurnId }), "Exact denied native command caused no remote file effect through provider retirement");
} else if (design.id !== "native-plan-reject-revise-accept") {
finalCheck("remote-no-workspace-effects", hasCursorRemoteWorkspaceUnchanged(snapshot, remoteBaseline!), "Native question or cancelled plan caused no remote workspace mutation through retirement");
}
@@ -197,7 +197,7 @@ export async function runCursorNativeFlow(input: {
if (watch) processes = observeProcesses();
const interactions = await api.get<Row[]>(`/api/issues/${issue.id}/interactions`);
const runEvents = runs.length === 1 ? await events(runs[0]!.id) : [];
if (design.id === "native-write-deny-reconnect" && runs.length === 1) denialNotices = readCursorToolEvidence(runEvents, runs[0]!.id);
if (design.id === "native-write-deny-reconnect" && runs.length === 1) { denialRunEvents = runEvents; denialNotices = readCursorToolEvidence(runEvents, runs[0]!.id); }
return { issue, runs, interactions, runEvents };
};
const reject = (state: Awaited<ReturnType<typeof load>>) => state.runs.length > 1 ? "Unexpected extra Cursor provider run" : state.runs.some(run => ["failed", "cancelled", "timed_out"].includes(run.status)) ? "Cursor provider run failed" : undefined;
@@ -305,7 +305,7 @@ export async function runCursorNativeFlow(input: {
check("browser-exact-denial", posted.turnId === event.turnId && posted.requestKind === "permission_approval" && posted.resolution?.action === "decline", "Browser denied the exact native run/request/turn");
const identity = { runId: native.runId, turnId: event.turnId, requestId: request.requestId, method: "session/request_permission" as const, action: "decline" as const };
await pollUntil({ label: "native denial delivered and exact command failed", deadlineAt: input.deadlineAt, load, reject, accept: state => hasDeliveredCursorNativeRequest({ ...identity, events: state.runEvents })
&& hasCursorDeniedCommand({ notices: denialNotices, ...identity, toolCallId: native.toolCallId, commandSha256: deniedCommand!.commandSha256, allowBootstrapReads: remote }) });
&& hasCursorDeniedCommand({ notices: denialNotices, ...identity, toolCallId: native.toolCallId, commandSha256: deniedCommand!.commandSha256, bootstrapReadProof: remoteFixture ? { actionFile: remoteFixture.actionFile, events: denialRunEvents } : undefined }) });
await sampleDenied("after-decision");
cancelRequestedAt = Date.now(); await api.post(`/api/heartbeat-runs/${native.runId}/cancel`);
const cancelled = await pollUntil({ label: "explicit native cancellation", deadlineAt: input.deadlineAt, load,
@@ -0,0 +1,117 @@
import { createHash } from "node:crypto";
import { expect, it } from "vitest";
import { bootstrapReadExecutionId, withoutProvenBootstrapReads, type BootstrapReadNotice } from "./native-bootstrap-read-proof.js";
const actionFile = `.paperclip-eval-action-${"a".repeat(36)}.txt`;
const origin: BootstrapReadNotice = { runId: "run", sessionId: "session", turnId: "turn", toolCallId: "tested", stage: "tool", operation: "execute", status: "pending", seq: 20 };
function read(status = "completed", seq = 1): BootstrapReadNotice { return { ...origin, toolCallId: "bootstrap", operation: "read", readTargetSha256: `sha256:${createHash("sha256").update(actionFile).digest("hex")}`, status, seq }; }
function receipt(type = "completed", status = "completed", seq = 2) {
return { runId: "run", seq, protocolSchemaVersion: 1, eventType: `tool.execution.${type}`, payload: { prpEvent: {
schema: "paperclip.prp.event.v1", schemaVersion: 1, sourceKind: "runner", runId: "run", turnId: "turn", eventType: `tool.execution.${type}`, emittedAt: new Date(seq).toISOString(),
payload: { schema: "paperclip.tool.execution.v1", executionId: "bootstrap", transport: "builtin", operation: "read", target: actionFile, status },
} } };
}
it.each(["completed", "failed"])("accepts an exact %s read even when the first native event is terminal", status => {
expect(withoutProvenBootstrapReads([read(status), origin], origin, { actionFile, events: [receipt("completed", status)] })).toEqual([origin]);
});
it("accepts a complete started/progressed/failed lifecycle, including failed unpublished-file reads", () => {
const rows = [read("pending", 1), read("in_progress", 3), read("failed", 5), origin];
const events = [receipt("started", "running", 2), receipt("progressed", "running", 4), receipt("completed", "failed", 6)];
expect(withoutProvenBootstrapReads(rows, origin, { actionFile, events })).toEqual([origin]);
});
it.each([
"wrong-target", "missing-target", "foreign-run", "foreign-turn", "foreign-row-run", "wrong-source", "wrong-schema", "wrong-payload", "wrong-transport", "wrong-operation", "wrong-event-type", "invalid-date", "duplicate-row", "duplicate-seq", "conflicting-target", "missing-row", "missing-terminal", "late-row", "unknown-read-origin",
])("rejects %s durable proof", variant => {
const row = receipt(), rows = [row]; const notices = [read(), origin]; const event = row.payload.prpEvent;
if (variant === "wrong-target") event.payload.target = "private.txt";
if (variant === "missing-target") delete (event.payload as any).target;
if (variant === "foreign-run") event.runId = "other";
if (variant === "foreign-row-run") row.runId = "other";
if (variant === "foreign-turn") event.turnId = "other";
if (variant === "wrong-source") event.sourceKind = "provider";
if (variant === "wrong-schema") event.schemaVersion = 2;
if (variant === "wrong-payload") event.payload.schema = "unknown";
if (variant === "wrong-transport") event.payload.transport = "mcp";
if (variant === "wrong-operation") event.payload.operation = "edit";
if (variant === "wrong-event-type") event.eventType = "tool.execution.progressed";
if (variant === "invalid-date") event.emittedAt = "unknown";
if (variant === "duplicate-row") rows.push(structuredClone(row));
if (variant === "duplicate-seq") row.seq = 1;
if (variant === "conflicting-target") { const other = receipt("completed", "completed", 3); other.payload.prpEvent.payload.target = "foreign.txt"; rows.push(other); }
if (variant === "missing-row") rows.length = 0;
if (variant === "missing-terminal") { rows[0] = receipt("started", "running"); notices[0] = read("pending"); }
if (variant === "late-row") row.seq = 21;
if (variant === "unknown-read-origin") event.payload.executionId = "unobserved-native-origin";
expect(() => withoutProvenBootstrapReads(notices, origin, { actionFile, events: rows })).toThrow(/proof/);
});
it("does not exempt unrelated earlier reads or infer a path from a title", () => {
const unrelated = { ...read(), toolCallId: "private-read", seq: 3 };
const wrong = receipt("completed", "completed", 4); wrong.payload.prpEvent.payload.executionId = "private-read"; wrong.payload.prpEvent.payload.target = "secrets.txt";
Object.assign(wrong.payload.prpEvent.payload, { name: `Read ${actionFile}` });
expect(() => withoutProvenBootstrapReads([read(), unrelated, origin], origin, { actionFile, events: [receipt(), wrong] })).toThrow();
expect(withoutProvenBootstrapReads([read(), unrelated, origin], origin)).toHaveLength(3);
});
it("rejects reused native IDs, cross-session notices and ambiguous completion ordering", () => {
for (const notices of [[read(), read("completed", 3), origin], [{ ...read(), sessionId: "other" }, origin], [read(), { ...read(), seq: 30 }, origin]]) {
expect(() => withoutProvenBootstrapReads(notices, origin, { actionFile, events: [receipt()] })).toThrow();
}
expect(() => withoutProvenBootstrapReads([read(), origin], origin, { actionFile: "guessed-nonce.txt", events: [receipt()] })).toThrow();
});
it("preserves later reads and unknown native kinds for the caller's extra-operation rejection", () => {
for (const n of [{ ...read(), seq: 21 }, { ...read(), operation: undefined }, { ...read(), operation: "edit" }]) {
expect(withoutProvenBootstrapReads([n, origin], origin, { actionFile, events: [] })).toEqual([n, origin]);
}
});
it("mirrors stable ASCII bounds and rejects invalid native identities", () => {
expect(bootstrapReadExecutionId("read:alpha-1")).toBe("read:alpha-1");
expect(bootstrapReadExecutionId("a".repeat(160))).toBe("a".repeat(160));
expect(bootstrapReadExecutionId("a".repeat(161))).toBe("acpx-tool-c4b0cf58ad5787f270b446a5eb28905e09d8e0aa06e7f577e27aed951d573406");
expect(bootstrapReadExecutionId("tool / é")).toBe("acpx-tool-c870bde596691facb0dc508859665ab3ee9de278e7d49adecaab0fa600feaff0");
expect(bootstrapReadExecutionId("é".repeat(121))).toBe("acpx-tool-9e1e0f61749be86786959ac0384b636ca4b9e00c78e9d77161f0432871edf151");
for (const id of ["", "a".repeat(241), "read\nnext", "[REDACTED]"]) expect(() => bootstrapReadExecutionId(id)).toThrow();
});
it("accepts omitted terminal path/kind only with the retained single-path origin attestation", () => {
const started = receipt("started", "running", 2), completed = receipt("completed", "failed", 4);
Object.assign(completed.payload.prpEvent.payload, { target: null, operation: "unknown" });
const notices = [read("pending", 1), read("failed", 3), origin], proof = { actionFile, events: [started, completed] };
expect(withoutProvenBootstrapReads(notices, origin, proof)).toEqual([origin]);
for (const digest of [undefined, `sha256:${"0".repeat(64)}`]) {
expect(() => withoutProvenBootstrapReads([{ ...notices[0]!, readTargetSha256: digest }, ...notices.slice(1)], origin, proof)).toThrow();
expect(() => withoutProvenBootstrapReads([notices[0]!, { ...notices[1]!, readTargetSha256: digest }, origin], origin, proof)).toThrow();
}
});
it.each(["cursor", "copilot"] as const)("correlates actual %s passive notices through the public reader and canonical omitted-field updates", async provider => {
const { createCursorToolEvidence } = await import("../../packages/paperclip-runner/src/drivers/acpx/cursor-tool-evidence.js");
const { createCopilotToolEvidence } = await import("../../packages/paperclip-runner/src/drivers/acpx/copilot-tool-evidence.js");
const { readCursorToolEvidence } = await import("./cursor-native-evidence.js");
const { readCopilotToolEvidence } = await import("./copilot-evidence.js");
const create = provider === "cursor" ? createCursorToolEvidence : createCopilotToolEvidence;
for (const variant of ["exact", "wrong-path", "multi-path", "ambiguous-update"]) {
const rows: any[] = [];
const projector = create({ sessionId: "session", turnId: "turn", workingDirectory: "/workspace", active: () => true,
emit: event => rows.push({ runId: "run", seq: rows.length * 2 + 1, protocolSchemaVersion: 1, eventType: event.eventType, payload: { prpEvent: {
schema: "paperclip.prp.event.v1", schemaVersion: 1, sourceKind: "runner", runId: "run", turnId: "turn", emittedAt: new Date(1).toISOString(), eventType: event.eventType, payload: event.payload,
} } }),
});
const path = variant === "wrong-path" ? "private.txt" : actionFile;
projector.tool({ type: "tool_call", tag: "tool_call", toolCallId: "bootstrap", kind: "read", status: "pending", rawInput: variant === "multi-path" ? { paths: [path, "private.txt"] } : { path }, locations: [{ path }] });
projector.tool({ type: "tool_call", tag: "tool_call_update", toolCallId: "bootstrap", status: "failed", ...(variant === "ambiguous-update" ? { rawInput: { path, paths: [path, "private.txt"] } } : {}) });
const evaluate = () => {
const readNotices = provider === "cursor" ? readCursorToolEvidence(rows, "run") : readCopilotToolEvidence(rows, "run");
const started = receipt("started", "running", 2), end = receipt("completed", "failed", 4);
Object.assign(end.payload.prpEvent.payload, { target: null, operation: "unknown" });
return withoutProvenBootstrapReads([...readNotices, origin], origin, { actionFile, events: [...rows, started, end] });
};
if (variant === "exact") expect(evaluate()).toEqual([origin]); else expect(evaluate).toThrow();
}
});
it("accepts bounded correlated progress for status-omitting native content updates", () => {
const notices = [read("pending", 1), read("completed", 5), origin];
const progressed = receipt("progressed", "running", 3); Object.assign(progressed.payload.prpEvent.payload, { target: null, operation: "unknown" });
const events = [receipt("started", "running", 2), progressed, receipt("completed", "completed", 6)];
expect(withoutProvenBootstrapReads(notices, origin, { actionFile, events })).toEqual([origin]);
progressed.payload.prpEvent.payload.target = "other.txt";
expect(() => withoutProvenBootstrapReads(notices, origin, { actionFile, events })).toThrow();
});
@@ -0,0 +1,89 @@
import { createHash } from "node:crypto";
export interface BootstrapReadNotice {
runId: string; sessionId: string; turnId: string; toolCallId: string; seq: number;
stage: string; operation?: string; status?: string; readTargetSha256?: string;
}
export interface BootstrapReadProof { actionFile: string; events: readonly unknown[] }
const record = (value: unknown): Record<string, any> => value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, any> : {};
const hashId = (domain: string, id: string) => `acpx-tool-${createHash("sha256").update(domain).update("tool\0").update(id).digest("hex")}`;
/** Mirror the pinned sidecar identity bound and Rust acpx_opaque_item_id. */
export function bootstrapReadExecutionId(toolCallId: string): string {
if (!toolCallId || toolCallId.length > 240 || /[\u0000-\u001f\u007f]/u.test(toolCallId) || toolCallId.includes("[REDACTED]")) throw new Error("Invalid bootstrap native tool identity");
const bounded = Buffer.byteLength(toolCallId) > 240 ? hashId("paperclip.acpx.provider-identity.v1\0", toolCallId) : toolCallId;
return /^[A-Za-z0-9][A-Za-z0-9._:-]*$/u.test(bounded) && Buffer.byteLength(bounded) <= 160 ? bounded : hashId("paperclip.acpx.opaque-item.v1\0", bounded);
}
/** Exempt only completed reads with the exact single-path native attestation on
* every notice. Canonical PRP retains only the first location and may omit path
* and kind on update, so it cannot establish single-file provenance by itself.
* Validate its correlated lifecycle and reject every conflicting explicit path;
* the passive origin attestation supplies cardinality and retained path proof.
* No target is inferred from titles, output, or the first location alone. */
export function withoutProvenBootstrapReads<T extends BootstrapReadNotice>(
notices: readonly T[], origin: BootstrapReadNotice, proof?: BootstrapReadProof,
): T[] {
if (!proof) return [...notices];
const expectedReadDigest = `sha256:${createHash("sha256").update(proof.actionFile).digest("hex")}`;
const invalid = () => new Error("Native bootstrap read lacks exact complete action-file proof");
if (notices.length > 2048 || proof.events.length > 20_000 || !/^\.paperclip-eval-action-[a-f0-9]{36}\.txt$/u.test(proof.actionFile) || !Number.isSafeInteger(origin.seq)) throw invalid();
const candidates = notices.filter(n => n.operation === "read" && n.seq < origin.seq);
const groups = new Map<string, T[]>();
const nativeIds = new Map<string, string>();
const noticeSeq = new Set<number>();
for (const n of candidates) {
if (n.readTargetSha256 !== expectedReadDigest || n.stage !== "tool" || n.runId !== origin.runId || n.turnId !== origin.turnId || n.sessionId !== origin.sessionId
|| !Number.isSafeInteger(n.seq) || n.seq < 0 || noticeSeq.has(n.seq)) throw invalid();
noticeSeq.add(n.seq);
const executionId = bootstrapReadExecutionId(n.toolCallId);
if (nativeIds.has(executionId) && nativeIds.get(executionId) !== n.toolCallId) throw invalid();
nativeIds.set(executionId, n.toolCallId);
const group = groups.get(executionId) ?? []; group.push(n); groups.set(executionId, group);
}
const receipts = new Map<string, Array<{ seq: number; type: string; status: string; operation: string }>>();
const receiptSeq = new Set<number>();
for (const value of proof.events) {
const row = record(value), event = record(record(row.payload).prpEvent), payload = record(event.payload);
const type = row.eventType;
const family = typeof type === "string" && type.startsWith("tool.execution.");
const correlated = groups.has(payload.executionId);
// An earlier canonical read without its native origin must not disappear.
const earlierRead = family && payload.operation === "read" && row.seq < origin.seq;
if (!correlated && !earlierRead) continue;
if (!family || !["tool.execution.started", "tool.execution.progressed", "tool.execution.completed"].includes(type)
|| row.runId !== origin.runId || row.protocolSchemaVersion !== 1 || event.schema !== "paperclip.prp.event.v1"
|| event.schemaVersion !== 1 || event.sourceKind !== "runner" || event.runId !== origin.runId || event.turnId !== origin.turnId
|| event.eventType !== type || !Number.isFinite(Date.parse(event.emittedAt))
|| payload.schema !== "paperclip.tool.execution.v1" || payload.transport !== "builtin" || !["read", "unknown"].includes(payload.operation)
|| (payload.target !== null && payload.target !== proof.actionFile) || !groups.has(payload.executionId)
|| !Number.isSafeInteger(row.seq) || row.seq < 0 || row.seq >= origin.seq || receiptSeq.has(row.seq) || noticeSeq.has(row.seq)) throw invalid();
receiptSeq.add(row.seq);
const group = receipts.get(payload.executionId) ?? [];
group.push({ seq: row.seq, type, status: payload.status, operation: payload.operation }); receipts.set(payload.executionId, group);
}
for (const [id, native] of groups) {
const canonical = receipts.get(id) ?? [];
native.sort((a, b) => a.seq - b.seq); canonical.sort((a, b) => a.seq - b.seq);
if (canonical.length < native.length || canonical.length === 0) throw invalid();
for (let index = 0; index < native.length; index++) {
const n = native[index]!;
const terminal = n.status === "completed" || n.status === "failed";
if ((!terminal && n.status !== "pending" && n.status !== "in_progress") || (terminal && index !== native.length - 1)) throw invalid();
}
if (!["completed", "failed"].includes(native.at(-1)!.status ?? "")) throw invalid();
for (let index = 0; index < canonical.length; index++) {
const c = canonical[index]!, last = index === canonical.length - 1;
if ((index === 0 && c.operation !== "read")
|| (last ? c.type !== "tool.execution.completed" || c.status !== native.at(-1)!.status
: c.status !== "running" || c.type !== (index === 0 ? "tool.execution.started" : "tool.execution.progressed"))) throw invalid();
}
// Native shape/content updates may omit status. PRP emits a progressed row
// while the passive observer retains its attestation without another notice.
// Such rows remain subject to exact identity, target, order and terminal checks.
// Any later notice for this origin means the supposed bootstrap was not
// complete before the tested action, even if an earlier terminal existed.
if (notices.some(n => n.toolCallId === native[0]!.toolCallId && !native.includes(n))) throw invalid();
}
const exempt = new Set(candidates);
return notices.filter(n => !exempt.has(n));
}