mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 00:53:38 +02:00
Preserve typed ACP read operations in native evidence
Keep setup reads distinguishable from unknown operations without retaining raw paths, arguments, or contents. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
133793b419
commit
84a52694af
4 files changed
+16
-4
No files matched your search
@@ -34,6 +34,16 @@ describe("Copilot active-turn tool evidence", () => {
|
||||
expect(JSON.stringify(events)).not.toMatch(/MUST NOT EXIST|diff --git|file_text/);
|
||||
expect(events.every(e => e.payload.scope === "turn" && (e.payload.provenance as { turnId: string }).turnId === "turn")).toBe(true);
|
||||
});
|
||||
it("preserves typed native reads without exposing read arguments or contents", () => {
|
||||
const h = harness();
|
||||
h.projector.tool(tool("read", { path: "instruction-secret", content: "private-content" }, "read"));
|
||||
h.projector.tool(update("read", "private-output"));
|
||||
expect(h.events.map(details)).toEqual([
|
||||
{ stage: "tool", toolCallId: "read", operation: "read", status: "pending" },
|
||||
{ stage: "tool", toolCallId: "read", operation: "read", status: "completed" },
|
||||
]);
|
||||
expect(JSON.stringify(h.events)).not.toMatch(/instruction-secret|private-content|private-output/);
|
||||
});
|
||||
it("links actual attached command and separate read-shell completion, not initial completed tool", () => {
|
||||
const ds = replay("attached-shell").events.map(details);
|
||||
const started = ds.find(d => d.shellState === "started")!; const completed = ds.find(d => d.shellState === "completed")!;
|
||||
|
||||
@@ -115,7 +115,7 @@ export function createCopilotToolEvidence(binding: {
|
||||
const status = ["pending", "in_progress", "completed", "failed"].includes(String(call.status)) ? String(call.status) : undefined;
|
||||
if (!status) return;
|
||||
const fields: Fields = { ...state.fields, status };
|
||||
if (state.kind === "edit") fields.operation = "edit";
|
||||
if (state.kind === "edit" || state.kind === "read") fields.operation = state.kind;
|
||||
const output = record(call.rawOutput).content;
|
||||
// Parse only the pinned native wrapper, never substrings in command output.
|
||||
if (status === "completed" && typeof output === "string" && output.length <= 256) {
|
||||
@@ -144,7 +144,7 @@ export function createCopilotToolEvidence(binding: {
|
||||
if (permissionTools.has(call.toolCallId) || permissionTools.size >= LIMIT) { notice("evidence_incomplete", call.toolCallId, { reason: "ambiguous_permission_origin" }); return undefined; }
|
||||
permissionTools.add(call.toolCallId);
|
||||
const fields: Fields = { requestId, ...inputFields(call) };
|
||||
if (call.kind === "edit") fields.operation = "edit";
|
||||
if (call.kind === "edit" || call.kind === "read") fields.operation = call.kind;
|
||||
fields.declineOffered = offeredActions.includes("decline");
|
||||
notice("permission_requested", call.toolCallId, fields, "session/request_permission");
|
||||
let delivered = false;
|
||||
|
||||
@@ -68,6 +68,8 @@ it("preserves execute denial evidence after valid edit and read permission input
|
||||
expect(s.unavailable()).toBe(false);
|
||||
expect(s.fields().some(row => row.stage === "evidence_incomplete")).toBe(false);
|
||||
expect(s.fields().filter(row => row.toolCallId !== "tool").every(row => row.commandSha256 === undefined)).toBe(true);
|
||||
expect(s.fields().filter(row => row.toolCallId === "read").every(row => row.operation === "read")).toBe(true);
|
||||
expect(s.fields().filter(row => row.toolCallId === "edit").every(row => row.operation === undefined)).toBe(true);
|
||||
const shell = s.fields().filter(row => row.toolCallId === "tool");
|
||||
expect(shell.map(row => row.stage)).toEqual(["tool", "permission_requested", "permission_delivered", "tool"]);
|
||||
expect(shell[2].outcome).toBe("reject_once"); expect(shell[3].status).toBe("failed");
|
||||
|
||||
@@ -51,7 +51,7 @@ export function createCursorToolEvidence(binding: {
|
||||
// Their bounded identity/status evidence must not disable later shell proof.
|
||||
if (state.kind === "execute" && permission.hasInput && (!permission.commandSha256 || permission.commandSha256 !== state.commandSha256)) throw new Error("Conflicting permission input");
|
||||
const fields: Fields = { requestId: permission.requestId, declineOffered: permission.declineOffered };
|
||||
if (state.kind === "execute") fields.operation = "execute";
|
||||
if (state.kind === "execute" || state.kind === "read") fields.operation = state.kind;
|
||||
if (state.commandSha256) fields.commandSha256 = state.commandSha256;
|
||||
if (!permission.requested) { notice("permission_requested", toolId, fields, "session/request_permission"); permission.requested = true; }
|
||||
if (permission.outcome && !permission.delivered) { notice("permission_delivered", toolId, { ...fields, outcome: permission.outcome }, "session/request_permission"); permission.delivered = true; }
|
||||
@@ -80,7 +80,7 @@ export function createCursorToolEvidence(binding: {
|
||||
if (!["pending", "in_progress", "completed", "failed"].includes(String(call.status))) return;
|
||||
// Publish queued permission delivery before its terminal tool result.
|
||||
if (call.tag !== "tool_call") flush(toolId);
|
||||
notice("tool", toolId, { status: String(call.status), ...(state.kind === "execute" ? { operation: "execute" } : {}), ...(state.commandSha256 ? { commandSha256: state.commandSha256 } : {}) });
|
||||
notice("tool", toolId, { status: String(call.status), ...(["execute", "read"].includes(state.kind ?? "") ? { operation: state.kind! } : {}), ...(state.commandSha256 ? { commandSha256: state.commandSha256 } : {}) });
|
||||
flush(toolId);
|
||||
}); },
|
||||
permission(request: unknown, requestId: string, offeredActions: readonly string[]) {
|
||||
|
||||
Reference in new issue
Block a user