Preserve Cursor command and denial evidence in both ACP transports

Observe only the active native prompt and bind permission delivery to the provider response write. Keep evidence failures separate from permission and terminal authority.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-29 16:49:15 -05:00
1 parent fb788a9145
commit 7fc2da7d34
4 files changed
+87 -10

No files matched your search

@@ -11,6 +11,9 @@ import { resolveQualifiedAcpxProfile } from "../drivers/acpx/qualified-profiles.
import { ACPX_SIDECAR_PROTOCOL_VERSION } from "../drivers/acpx/sidecar-protocol.js";
import { canonicalProviderEventsFromAcpxRuntimeEvent } from "../provider-events.js";
import { createPiMessageProjection } from "../drivers/acpx/pi-message-projection.js";
import { createCopilotToolEvidence } from "../drivers/acpx/copilot-tool-evidence.js";
import { createCursorToolEvidence } from "../drivers/acpx/cursor-tool-evidence.js";
import { validateAcpxRichEvent } from "../drivers/acpx/profile-extensions.js";
import {
awaitSidecarCleanupWithin,
closeActiveSidecarHostWithin,
@@ -36,6 +39,32 @@ afterEach(async () => {
});
describe("qualified ACPX runtime sidecar", () => {
it.each(["cursor", "copilot", "pi"])("binds native tool evidence to the active sidecar turn for %s", agent => {
const source = readFileSync(new URL("./acpx-runtime-sidecar.ts", import.meta.url), "utf8");
const start = source.indexOf(" const evidenceFactory =");
const end = source.indexOf(" let usageBefore:", start);
expect(start).toBeGreaterThan(0);
const emitted: unknown[] = [];
const create = new Function("createCopilotToolEvidence", "createCursorToolEvidence", "validateAcpxRichEvent", "emit", "agent", `
const activeHost = { identity: () => ({ backendSessionId: "session" }) };
let host = activeHost, turnId = "turn";
const currentTurnId = "turn", openParams = { agent, workingDirectory: "/workspace" };
const diagnostic = () => {};
${source.slice(start, end).replaceAll("openParams!", "openParams")}
return { evidence: toolEvidence, retire: () => { turnId = null; } };
`)(createCopilotToolEvidence, createCursorToolEvidence, validateAcpxRichEvent, (...args: unknown[]) => emitted.push(args), agent);
const tool = { type: "tool_call", tag: "tool_call", toolCallId: "tool", kind: "execute", status: "pending", rawInput: { command: "printf private-value" } };
create.evidence?.tool(tool);
expect(emitted).toHaveLength(agent === "pi" ? 0 : 1);
if (agent !== "pi") expect(emitted[0]).toEqual(["runtime.rich_event", expect.objectContaining({ payload: expect.objectContaining({
category: `${agent}_tool_evidence_v1`, provenance: expect.objectContaining({ sessionId: "session", turnId: "turn" }),
}) }), "turn"]);
expect(JSON.stringify(emitted)).not.toContain("private-value");
create.retire();
create.evidence?.tool({ ...tool, tag: "tool_call_update", status: "failed" });
expect(emitted).toHaveLength(agent === "pi" ? 0 : 1);
});
it("passes only validated Pi native boundaries and history through the real text sanitizer", () => {
const source = readFileSync(fileURLToPath(new URL("./acpx-runtime-sidecar.ts", import.meta.url)), "utf8");
const start = source.indexOf(' if (event.type === "text_delta") {', source.indexOf("function sanitizeRuntimeEvent"));
@@ -63,6 +63,7 @@ import {
} from "../drivers/acpx/sidecar-protocol.js";
import { safeAcpxLocations } from "./acpx-sidecar-locations.js";
import { createCopilotToolEvidence, type CopilotToolEvidence } from "../drivers/acpx/copilot-tool-evidence.js";
import { createCursorToolEvidence, type CursorToolEvidence } from "../drivers/acpx/cursor-tool-evidence.js";
import {
persistedAcpxTurnUsage,
acpxUsageEstimateNotice,
@@ -370,13 +371,15 @@ async function dispatch(
waitForInput: (input, context) => waitForExtensionInput(currentTurnId, input, context),
emit: event => emit("runtime.rich_event", { ...event }, currentTurnId),
});
const toolEvidence = openParams!.agent === "copilot" ? createCopilotToolEvidence({
const evidenceFactory = openParams!.agent === "copilot" ? createCopilotToolEvidence
: openParams!.agent === "cursor" ? createCursorToolEvidence : undefined;
const toolEvidence = evidenceFactory?.({
sessionId: activeHost.identity().backendSessionId, turnId: currentTurnId,
workingDirectory: openParams!.workingDirectory,
active: () => turnId === currentTurnId && host === activeHost,
emit: event => { validateAcpxRichEvent(event); emit("runtime.rich_event", { ...event }, currentTurnId); },
unavailable: () => diagnostic("copilot_evidence_unavailable", "Copilot tool evidence is incomplete; permission and terminal outcomes are unchanged."),
}) : undefined;
unavailable: () => diagnostic(`${openParams!.agent}_evidence_unavailable`, "ACP tool evidence is incomplete; permission and terminal outcomes are unchanged."),
});
let usageBefore: unknown;
try {
usageBefore = await readSidecarHostStatusWithin(activeHost);
@@ -602,7 +605,7 @@ async function pumpTurn(
activeHost: AcpxRuntimeHost,
usageBefore: unknown,
drainExtensions: () => Promise<void>,
toolEvidence?: CopilotToolEvidence,
toolEvidence?: CopilotToolEvidence | CursorToolEvidence,
): Promise<void> {
let terminal: Record<string, unknown>;
try {
@@ -757,7 +760,7 @@ async function waitForPermission(
activeTurnId: string,
request: AcpPermissionRequest,
context: { signal: AbortSignal; responseDelivery?: Promise<void> },
toolEvidence?: CopilotToolEvidence,
toolEvidence?: CopilotToolEvidence | CursorToolEvidence,
): Promise<AcpPermissionDecision> {
const { signal } = context;
if (turnId !== activeTurnId || signal.aborted || permissions.size >= MAX_PENDING_INPUTS) {
@@ -1,4 +1,5 @@
import { readFileSync } from "node:fs";
import { createHash } from "node:crypto";
import { describe, expect, it, vi } from "vitest";
import type { AcpRuntimeEvent } from "acpx/runtime";
@@ -1606,6 +1607,47 @@ describe("Codex ACPX harness driver", () => {
await session.close({ reason: "receipt checked" });
});
it.each(["written", "failed"] as const)("binds Cursor denial evidence to its original tool and response write: %s", async outcome => {
const command = "printf 'sensitive-value' > /workspace/denied.txt";
const fixture = driverFixture({ agent: "cursor", model: "explicit-test-model", providerPolicy: { readOnly: false } }, {
runtimeEvents: [{ type: "tool_call", tag: "tool_call", toolCallId: "cursor-tool", title: "Run command", kind: "execute", status: "pending", rawInput: { command } }],
});
const session = await fixture.driver.openSession({ runId: "run-cursor-receipt", normalizedSessionId: "session-1", workingDirectory: "/workspace" });
const origin = collectUntil(session.events(), "provider.notice.recorded");
const { turnId } = await session.startTurn({ message: { text: "Request the command." } });
const originEvents = await origin;
const created = collectUntil(session.events(), "runtime_request.created");
const callback = fixture.host.startTurn.mock.calls[0]![0].onPermissionRequest!;
const receipt = deferred<void>();
const providerResponse = callback({ inferredKind: "execute", raw: {
sessionId: "backend-1", toolCall: { toolCallId: "cursor-tool", title: "Run command", kind: "execute" },
options: [{ optionId: "deny", kind: "reject_once", name: "Deny" }],
} } as Parameters<typeof callback>[0], { signal: new AbortController().signal, responseDelivery: receipt.promise });
const requested = await created;
const request = session.pendingRuntimeRequests!()[0]!;
const evidence = (events: PrpEvent[]) => events.filter(event => event.eventType === "provider.notice.recorded" && event.payload.category === "cursor_tool_evidence_v1");
const fields = (event: PrpEvent) => Object.fromEntries((event.payload.details as Array<{ name: string; value: string }>).map(field => [field.name, field.value]));
const commandSha256 = `sha256:${createHash("sha256").update(command).digest("hex")}`;
expect(evidence(requested).map(fields)).toEqual([expect.objectContaining({ stage: "permission_requested", toolCallId: "cursor-tool", requestId: request.requestId, commandSha256, declineOffered: "true" })]);
const settled = collectUntil(session.events(), outcome === "written" ? "runtime_request.resolved" : "runtime_request.expired");
let acknowledged = false;
const resolution = session.resolveRuntimeRequest!({ requestId: request.requestId, turnId, resolution: { action: "decline" } }).then(() => { acknowledged = true; });
await expect(providerResponse).resolves.toEqual({ outcome: "reject_once" });
expect(acknowledged).toBe(false);
if (outcome === "written") { receipt.resolve(); await resolution; }
else {
const failure = expect(resolution).rejects.toThrow("pipe failed");
receipt.reject(new Error("pipe failed")); await failure;
}
const terminalEvents = await settled;
expect(evidence(terminalEvents).map(fields)).toEqual(outcome === "written"
? [expect.objectContaining({ stage: "permission_delivered", outcome: "reject_once", commandSha256, requestId: request.requestId })] : []);
expect(JSON.stringify([...evidence(originEvents), ...evidence(requested), ...evidence(terminalEvents)])).not.toContain("sensitive-value");
expect(session.pendingRuntimeRequests!()).toHaveLength(0);
if (outcome === "written") fixture.finishTurn({ status: "completed", stopReason: "end_turn" });
await session.close({ reason: "Cursor receipt verified" });
});
it.each(["copilot", "codex"] as const)("preserves pinned attached-shell evidence only on the Copilot direct driver: %s", async agent => {
const wire = JSON.parse(readFileSync(new URL("./fixtures/copilot-tool-evidence.json", import.meta.url), "utf8"))["attached-shell"];
const runtimeEvents = wire.filter((frame: any) => frame.method === "session/update").map((frame: any) => ({ ...frame.params.update, type: "tool_call", tag: frame.params.update.sessionUpdate })) as AcpRuntimeEvent[];
@@ -1,4 +1,5 @@
import { createCopilotToolEvidence, type CopilotToolEvidence } from "./copilot-tool-evidence.js";
import { createCursorToolEvidence, type CursorToolEvidence } from "./cursor-tool-evidence.js";
import { requireAcpxResponseDelivery } from "./response-delivery.js";
import { createPiMessageProjection, piBoundaryClearsFinal, type PiProjectedMessageEvent } from "./pi-message-projection.js";
import { acpxProfileClientCapabilities, bindAcpxExtensionTurn, validateAcpxRichEvent, createAcpxProfileExtensionAdapter, type AcpxExtensionInput } from "./profile-extensions.js";
@@ -901,14 +902,16 @@ class CodexAcpxSession implements HarnessSession {
}
},
});
const toolEvidence = this.#agent === "copilot" ? createCopilotToolEvidence({
const evidenceFactory = this.#agent === "copilot" ? createCopilotToolEvidence
: this.#agent === "cursor" ? createCursorToolEvidence : undefined;
const toolEvidence = evidenceFactory?.({
sessionId: this.#host.identity().backendSessionId, turnId, workingDirectory: this.#input.workingDirectory,
active: () => this.#activeTurnId === turnId && !this.#closingStarted,
emit: event => {
validateAcpxRichEvent(event);
if (!this.#emit(event.eventType, event.payload, { turnId, itemId: event.itemId })) throw new Error("Copilot activity could not be retained");
if (!this.#emit(event.eventType, event.payload, { turnId, itemId: event.itemId })) throw new Error("ACP tool activity could not be retained");
},
}) : undefined;
});
let turn: AcpxRuntimeTurn;
const usageBefore = await readUsageStatus(this.#host);
try {
@@ -1438,7 +1441,7 @@ class CodexAcpxSession implements HarnessSession {
.catch(() => undefined);
}
async #pumpTurn(turnId: string, turn: AcpxRuntimeTurn, drainExtensions: () => Promise<void>, usageBefore: unknown, toolEvidence?: CopilotToolEvidence): Promise<void> {
async #pumpTurn(turnId: string, turn: AcpxRuntimeTurn, drainExtensions: () => Promise<void>, usageBefore: unknown, toolEvidence?: CopilotToolEvidence | CursorToolEvidence): Promise<void> {
try {
let index = 0;
const normalizeToolEvent =
@@ -1728,7 +1731,7 @@ class CodexAcpxSession implements HarnessSession {
turnId: string,
request: AcpPermissionRequest,
context: { signal: AbortSignal; responseDelivery?: Promise<void> },
toolEvidence?: CopilotToolEvidence,
toolEvidence?: CopilotToolEvidence | CursorToolEvidence,
): Promise<AcpPermissionDecision> {
const { signal } = context;
if (this.#closed || this.#activeTurnId !== turnId || signal.aborted