diff --git a/packages/paperclip-runner/src/drivers/acpx/copilot-tool-evidence.test.ts b/packages/paperclip-runner/src/drivers/acpx/copilot-tool-evidence.test.ts index a421ffa73ca..1db0de9a711 100644 --- a/packages/paperclip-runner/src/drivers/acpx/copilot-tool-evidence.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/copilot-tool-evidence.test.ts @@ -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")!; diff --git a/packages/paperclip-runner/src/drivers/acpx/copilot-tool-evidence.ts b/packages/paperclip-runner/src/drivers/acpx/copilot-tool-evidence.ts index 9e173dc5102..e82d3561425 100644 --- a/packages/paperclip-runner/src/drivers/acpx/copilot-tool-evidence.ts +++ b/packages/paperclip-runner/src/drivers/acpx/copilot-tool-evidence.ts @@ -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; diff --git a/packages/paperclip-runner/src/drivers/acpx/cursor-tool-evidence.test.ts b/packages/paperclip-runner/src/drivers/acpx/cursor-tool-evidence.test.ts index d761f2a077b..0f73f7c201d 100644 --- a/packages/paperclip-runner/src/drivers/acpx/cursor-tool-evidence.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/cursor-tool-evidence.test.ts @@ -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"); diff --git a/packages/paperclip-runner/src/drivers/acpx/cursor-tool-evidence.ts b/packages/paperclip-runner/src/drivers/acpx/cursor-tool-evidence.ts index 12647937e76..fe8d0f5c7be 100644 --- a/packages/paperclip-runner/src/drivers/acpx/cursor-tool-evidence.ts +++ b/packages/paperclip-runner/src/drivers/acpx/cursor-tool-evidence.ts @@ -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[]) {