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:
DottaandPaperclip committed 2026-09-29 17:26:29 -05:00
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[]) {