Files
PaperClipAI/tests/runner-e2e/pi-bootstrap-permission.test.ts
T

77 lines
7.3 KiB
TypeScript

import { createHash } from "node:crypto";
import { describe, expect, it } from "vitest";
import { canonicalJson } from "../../packages/shared/src/portability-hash.js";
import { approvePiBootstrapRead, withoutApprovedPiBootstrapRequests, type PiBootstrapApproval } from "./pi-bootstrap-permission.js";
import { observePiControlPending, readPiStopSettlement, readPiSteeringSettlement } from "./pi-controls-evidence.js";
import { piCancellationId, piControlCaller, piControlFixture } from "./pi-controls-test-fixture.js";
const actionFile = `.paperclip-eval-action-${"a".repeat(36)}.txt`;
const digest = (value: unknown) => `sha256:${createHash("sha256").update(canonicalJson(value)).digest("hex")}`;
function fixture() {
const f = piControlFixture(), write = structuredClone(f.events);
f.events.length = 0;
const read = { schema: "paperclip.tool.execution.v1", transport: "builtin", executionId: "setup-tool", name: "read", operation: "read", target: actionFile, status: "running" };
const request = { ...f.request, requestId: "setup-request", itemId: "setup-item", details: { toolCallId: "setup-tool" }, choices: [{ key: "accept", label: "Allow once" }, { key: "decline", label: "Deny" }] };
f.append("tool.execution.started", read); f.append("runtime_request.created", { request });
const finish = () => {
f.append("runtime_request.resolved", { requestId: request.requestId, turnId: "turn", action: "accept" });
f.append("tool.execution.completed", { ...read, status: "completed" });
for (const row of write) f.append(row.eventType, row.payload.prpEvent.payload);
};
const proof = (): PiBootstrapApproval => ({ schema: "paperclip.e2e.pi-bootstrap-read-approval.v1", companyId: f.scope.companyId, issueId: f.scope.issueId, runId: f.scope.runId, actionFile, publishedSha256: `sha256:${"b".repeat(64)}`,
requestId: "setup-request", toolCallId: "setup-tool", turnId: "turn", normalizedSessionId: "session", sourceInstanceId: "source",
createdRowSha256: digest(f.events[1]), resolvedRowSha256: digest(f.events[2]), completedRowSha256: digest(f.events[3]) });
return { ...f, read, finish, proof };
}
describe("Pi operator setup read", () => {
it("approves only the published owned file through the exact public request", async () => {
const f = fixture(), saved = new Map<string, unknown>(); let posts = 0;
const result = await approvePiBootstrapRead({
api: { post: async (path: string, data: unknown) => { expect(path).toBe("/api/heartbeat-runs/run/runtime-requests/setup-request/resolve"); expect(data).toEqual({ turnId: "turn", requestKind: "permission_approval", resolution: { action: "accept" } }); posts++; f.finish(); } } as any,
fixture: { actionFile, binding: { companyId: "company", runId: "run" }, snapshot: async () => ({ complete: true, setup: { path: actionFile, published: true, sha256: `sha256:${"b".repeat(64)}` } }) } as any,
companyId: "company", issueId: "issue", runId: "run", deadlineAt: Date.now() + 2000, load: async () => f.state(), evidence: async (name, value) => { saved.set(name, value); },
});
expect(posts).toBe(1); expect(result).toEqual(f.proof()); expect(saved.has("pi-bootstrap-read-before-approval.json")).toBe(true);
const pending = observePiControlPending({ ...f.state(), scope: f.scope, bootstrapApproval: result });
expect(pending.requestId).toBe("request"); expect(pending.bootstrapApproval).toEqual(result);
});
it.each(["unpublished", "foreign-binding", "other-file", "shell"])("does not approve %s setup", async failure => {
const f = fixture(); let posts = 0;
if (failure === "other-file") f.events[0]!.payload.prpEvent.payload.target = "private.txt";
if (failure === "shell") { f.events[0]!.payload.prpEvent.payload.name = "bash"; f.events[0]!.payload.prpEvent.payload.operation = "execute"; }
await expect(approvePiBootstrapRead({ api: { post: async () => { posts++; } } as any,
fixture: { actionFile, binding: { companyId: failure === "foreign-binding" ? "other" : "company", runId: "run" }, snapshot: async () => ({ complete: true, setup: { path: actionFile, published: failure !== "unpublished", sha256: `sha256:${"b".repeat(64)}` } }) } as any,
companyId: "company", issueId: "issue", runId: "run", deadlineAt: Date.now() + 2000, load: async () => f.state(), evidence: async () => {},
})).rejects.toThrow("Pi bootstrap"); expect(posts).toBe(0);
});
it.each(["stop", "steering"])("retains the original %s assertions after a proven setup read", mode => {
const f = fixture(); f.finish(); const proof = f.proof();
expect(() => observePiControlPending({ ...f.state(), scope: f.scope })).toThrow("one native permission");
const pending = observePiControlPending({ ...f.state(), scope: f.scope, bootstrapApproval: proof });
if (mode === "stop") { f.cancel(); expect(readPiStopSettlement({ ...f.state(), pending, caller: piControlCaller, cancellationRequestId: piCancellationId, dispatchMonotonicNs: (BigInt(pending.observedMonotonicNs) + 1n).toString() }).normalCompletionAccepted).toBe(false); }
else { f.steer();
// The wrapper's setup finish is separate from the original write finish.
f.append("runtime_request.resolved", { requestId: "request", turnId: "turn", action: "decline" });
f.append("tool.execution.completed", { ...f.tool, status: "failed", output: "Pi operation was denied or cancelled" });
f.append("turn.completed", { status: "completed", error: null }); f.run.status = "succeeded"; f.issue.status = "done";
f.append("run.result.accepted", { result: { schema: "paperclip.run_result.v1", reportedWorkDisposition: "done", summary: f.marker } }, { sourceKind: "control_plane", sourceInstanceId: "source:control", sourceSeq: 1, sourceEventId: "source:control:run:1" });
f.append("run.terminal", { schema: "paperclip.prp.terminal.v1", runTerminalState: "succeeded", turnTerminalState: "completed", reportedWorkDisposition: "done" }, { sourceKind: "control_plane", sourceInstanceId: "source:control", sourceSeq: 2, sourceEventId: "source:control:run:2" });
expect(readPiSteeringSettlement({ ...f.state(), pending, commentId: f.commentId, queueId: f.queueId, marker: f.marker, finalMessage: f.marker }).marker).toBe(f.marker);
}
});
it.each(["changed-receipt", "wrong-path", "failed-read", "extra-permission", "reordered", "allow-always"])("rejects %s before excluding setup records", failure => {
const f = fixture(); f.finish(); const proof = f.proof();
if (failure === "changed-receipt") proof.resolvedRowSha256 = `sha256:${"0".repeat(64)}`;
if (failure === "wrong-path") f.events[0]!.payload.prpEvent.payload.target = "other.txt";
if (failure === "failed-read") f.events[3]!.payload.prpEvent.payload.status = "failed";
if (failure === "extra-permission") f.append("runtime_request.created", { request: { ...f.request, requestId: "extra" } });
if (failure === "reordered") f.events[3]!.seq = 1;
if (failure === "allow-always") f.events[2]!.payload.prpEvent.payload.action = "accept_always";
expect(() => observePiControlPending({ ...f.state(), scope: f.scope, bootstrapApproval: proof })).toThrow();
});
it("does not discard any native lifecycle row", () => { const f = fixture(); f.finish(); const rows = withoutApprovedPiBootstrapRequests(f.events, f.proof()); expect(rows).toHaveLength(f.events.length - 2); expect(rows.filter((row: any) => row.eventType.startsWith("tool.execution."))).toHaveLength(3); });
});