mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 00:05:49 +02:00
test: require exact attested bootstrap reads in native qualification
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
b41ff0377b
commit
794e90a258
12 files changed
+278
-33
No files matched your search
@@ -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.
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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]!;
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
Reference in new issue
Block a user