fix(runner): align Cursor control IDs and retain native receipt evidence

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-30 22:11:14 -05:00
1 parent 8ec354f609
commit 041d84760f
20 files changed
+219 -41

No files matched your search

@@ -15,7 +15,12 @@ its repeated text fields. Large and chunked bridge results still carry a receipt
but output above that bound has no native correlation evidence. Error results
retain `isError`; deterministic tests cover errors, while the actual pinned
ARM64 executable has only been checked with a small two-block success response
and a local mock model. This is not paid or cross-platform qualification.
and a local mock model. The captured pending/completed ACP frames are retained
in `src/drivers/acpx/fixtures/copilot-1.0.88-mcp-receipt-captured.json`, with
executable/capture hashes and sanitization recorded alongside. A regression
replays those frames through the production projector with the separately
owned receipt, plus missing-authority, altered-input/result, missing-content
and duplicate-terminal negatives. This is not paid or cross-platform qualification.
Native executable and distribution bytes are unchanged. The profile binds the
receipt, bridge and projector sources plus the complete permission-policy import
closure; retained v9 sessions are incompatible. Fresh packs and local/Daytona
@@ -1,9 +1,15 @@
# Cursor ACP capability inventory
Current source candidate (2026-10-01): **Cursor profile v10 is unqualified**.
Native tool IDs containing control characters now use the same bounded hash in
tool activity, permission details and correlated evidence. Safe IDs stay unchanged;
distinct native IDs stay distinct. The original ACP request and option identity
Native tool IDs containing C0, C1 or DEL control characters now use the same
bounded hash in permission details, passive evidence and the sidecar tool event.
Blank permission IDs are rejected because ACPX drops blank tool-event identity.
Other admitted IDs stay unchanged at this boundary; distinct IDs stay distinct.
Canonical tool execution then applies the existing Rust opaque-ID conversion.
For example, `tool/1` remains the native permission/evidence key while its
execution ID is the deterministic opaque hash. Lifecycle readers explicitly
convert between these keys; they must not compare them directly. Both-order
bridge tests cover this distinction, including 161-character IDs. The original ACP request and option identity
remain intact for response delivery. The declaration binds the identity helper,
permission adapter and evidence projector. Retained v9 sessions are incompatible.
The native distribution and its usage limits are unchanged. Deterministic tests
@@ -1,8 +1,9 @@
# Rich ACP integration and qualification report
Current source checkpoint (2026-10-01): **Cursor v10, Copilot v10 and Pi v10
remain unqualified**. Cursor now shares the exact opaque native tool-ID mapping
across activity and permissions. Copilot correlates native tool results with
remain unqualified**. Cursor now shares the bounded native tool-ID mapping
across sidecar activity and permissions, then deterministically joins that key
to the canonical opaque execution ID. Copilot correlates native tool results with
authenticated, turn-bound semantic receipts instead of display names. Both new
profile identities reject v9 warm sessions. Their native distribution bytes
are unchanged; new sidecar assets, runner admission digests and fresh local/Daytona
@@ -215,7 +215,7 @@ impl AcpxProviderDescriptor {
"2026.09.26-dd393fe",
None,
None,
"sha256:e4ffecec5a7d43252f7f79cd681df582492b41a40dd33c1ad1a8354a178ccddb",
"sha256:1df2a15b93bc3a14fa47fa3315344ba023fe2412048047cdc6f32096a6336564",
),
"copilot" => (
self.model.as_str(),
@@ -223,7 +223,7 @@ impl AcpxProviderDescriptor {
"1.0.88",
None,
None,
"sha256:2d501724197e00564fb8cd8ce7b0f951c32fb6c1d3bfdde2dd0531171e3db266",
"sha256:c6741d7e49cd1af4ecad9e07181ef96305bde48b56dce2958547f2f066666231",
),
"grok" => (
"grok-4.7",
@@ -2803,7 +2803,7 @@ mod tests {
"cursor",
"cursor-agent",
"2026.09.26-dd393fe",
"sha256:e4ffecec5a7d43252f7f79cd681df582492b41a40dd33c1ad1a8354a178ccddb",
"sha256:1df2a15b93bc3a14fa47fa3315344ba023fe2412048047cdc6f32096a6336564",
None,
None,
"explicit-model",
@@ -2812,7 +2812,7 @@ mod tests {
"copilot",
"@github/copilot",
"1.0.88",
"sha256:2d501724197e00564fb8cd8ce7b0f951c32fb6c1d3bfdde2dd0531171e3db266",
"sha256:c6741d7e49cd1af4ecad9e07181ef96305bde48b56dce2958547f2f066666231",
None,
None,
"explicit-model",
@@ -7,7 +7,7 @@ import { COPILOT_VERSION, materializePinnedCopilotBinary, resolveCopilotDistribu
const MAX_ARCHIVE_BYTES = 128 * 1024 * 1024;
const MAX_EXPANDED_BYTES = 384 * 1024 * 1024;
const PROFILE_DIGEST = "sha256:2d501724197e00564fb8cd8ce7b0f951c32fb6c1d3bfdde2dd0531171e3db266";
const PROFILE_DIGEST = "sha256:c6741d7e49cd1af4ecad9e07181ef96305bde48b56dce2958547f2f066666231";
/** Build-time only; outputRoot is the exact runner-owned platform asset directory. */
export async function buildPinnedCopilotDistribution({ outputRoot, platform = process.platform, architecture = process.arch }, { fetchImpl = fetch } = {}) {
@@ -28,7 +28,7 @@ export async function materializeCandidateProviderPack({ provider, outputRoot })
if (provider === "cursor") {
const result = await materializePinnedCursorDistribution({ destination: outputRoot });
return { version: result.version,
profileDigest: "sha256:e4ffecec5a7d43252f7f79cd681df582492b41a40dd33c1ad1a8354a178ccddb",
profileDigest: "sha256:1df2a15b93bc3a14fa47fa3315344ba023fe2412048047cdc6f32096a6336564",
closureDigest: `sha256:${result.closureSha256}` };
}
throw new Error(`The ${provider} candidate distribution builder is not included in this source revision`);
@@ -111,7 +111,7 @@ describe("qualified ACPX runtime sidecar", () => {
const start = source.indexOf(" const { signal } = context;", source.indexOf("async function waitForPermission"));
const end = source.indexOf("\nasync function waitForInput", start);
const observedIds: string[] = [];
for (const rawId of ["native\u0000tool", "native\u007ftool", "safe-tool-1"]) {
for (const rawId of ["native\u0000tool", "native\u007ftool", "native\u0085tool", "tool/1", "x".repeat(161), "safe-tool-1"]) {
const permissions = new Map<string, any>(); const emitted: any[] = []; const notices: any[] = [];
const evidence = createCursorToolEvidence({ sessionId: "session", turnId: "turn-1", workingDirectory: "/workspace", active: () => true,
emit: event => { validateAcpxRichEvent(event); notices.push(event); },
@@ -122,7 +122,10 @@ describe("qualified ACPX runtime sidecar", () => {
return async function(activeTurnId, agent, request, context, toolEvidence) { ${source.slice(start, end)}
`)(permissions, normalizeAcpxPermission, (_event: string, payload: unknown) => emitted.push(payload));
const origin = { type: "tool_call", tag: "tool_call", toolCallId: rawId, kind: "execute", status: "pending", rawInput: { command: "printf private-command" } };
const activity = canonicalProviderEventsFromAcpxRuntimeEvent(bound(origin), "unrelated-fallback")[0]!;
// The sidecar emits this bounded runtime event; Rust applies the opaque
// execution-ID transform later. The direct TS driver normalizer is not
// this boundary and has a different fallback policy.
const activity = bound(origin);
const native = { sessionId: "session", inferredKind: "execute", raw: { sessionId: "session", toolCall: { toolCallId: rawId, kind: "execute" },
options: [{ kind: "reject_once", optionId: "native-original-denial", name: "Deny" }],
} };
@@ -133,7 +136,7 @@ describe("qualified ACPX runtime sidecar", () => {
if (order === "permission-first") { expect(notices).toEqual([]); evidence.tool(origin); }
const projectedId = emitted[0].toolCallId;
observedIds.push(projectedId);
expect(projectedId).toBe(activity.payload.executionId);
expect(projectedId).toBe(activity.toolCallId);
expect(notices.map(event => Object.fromEntries(event.payload.details.map((field: any) => [field.name, field.value])))).toEqual([
expect.objectContaining({ stage: "tool", toolCallId: projectedId }),
expect.objectContaining({ stage: "permission_requested", toolCallId: projectedId, requestId: "request-1" }),
@@ -146,8 +149,8 @@ describe("qualified ACPX runtime sidecar", () => {
expect(native.raw.toolCall.toolCallId).toBe(rawId);
expect(JSON.stringify([activity, emitted, notices])).not.toContain("private-command");
}
expect(new Set(observedIds).size).toBe(3);
expect(observedIds[2]).toBe("safe-tool-1");
expect(new Set(observedIds).size).toBe(6);
expect(observedIds.at(-1)).toBe("safe-tool-1");
});
it("emits the native plan tool identity from the actual sidecar input boundary", async () => {
const source = readFileSync(new URL("./acpx-runtime-sidecar.ts", import.meta.url), "utf8");
@@ -13,7 +13,7 @@ describe("ACP permission normalization", () => {
const native = request(); native.raw.toolCall.toolCallId = "native\u0000tool";
expect(normalizeAcpxPermission(native, { provider }).toolCallId).toBe("native\u0000tool");
});
it.each([undefined, null, "", "x".repeat(241)])("rejects missing or oversized Cursor identities without deriving one from other fields", toolCallId => {
it.each([undefined, null, "", " ", "x".repeat(241)])("rejects missing or oversized Cursor identities without deriving one from other fields", toolCallId => {
const native = request();
Object.assign(native.raw.toolCall, { toolCallId, itemId: "fallback", title: "safe-tool" });
expect(() => normalizeAcpxPermission(native, { provider: "cursor" })).toThrow("tool identity");
@@ -33,7 +33,8 @@ export function normalizeAcpxPermission(request: AcpPermissionRequest, options:
byKind.set(option.kind, { optionId: option.optionId, name: option.name });
}
const call = raw.toolCall;
if (!call || typeof call.toolCallId !== "string" || !call.toolCallId || call.toolCallId.length > 240) {
if (!call || typeof call.toolCallId !== "string" || !call.toolCallId || call.toolCallId.length > 240
|| (options.provider === "cursor" && !call.toolCallId.trim())) {
throw new Error("ACP permission request omitted its tool identity");
}
const needsEditContext = options.provider === "copilot" && call.kind === "edit";
@@ -167,3 +167,42 @@ describe("Copilot authoritative semantic receipt correlation", () => {
expect(details(next.events.at(-1)!)).not.toHaveProperty("semanticOperationId");
});
});
describe("captured Copilot 1.0.88 native MCP receipt carrier", () => {
const bytes = readFileSync(new URL("./fixtures/copilot-1.0.88-mcp-receipt-captured.json", import.meta.url));
const captured = JSON.parse(bytes.toString());
const provenance = JSON.parse(readFileSync(new URL("./fixtures/copilot-1.0.88-mcp-receipt-captured.provenance.json", import.meta.url), "utf8"));
const nativeEvent = (frame: any) => ({ ...frame.params.update, type: "tool_call", tag: frame.params.update.sessionUpdate });
it("replays native frames with the separately owned receipt and preserves rejection content", () => {
expect(createHash("sha256").update(bytes).digest("hex")).toBe(provenance.fixtureSha256);
expect(provenance).toMatchObject({ nativeVersion: "1.0.88", paidProviderCalls: 0, qualification: false });
expect(captured.pending.params.sessionId).toBe(captured.completed.params.sessionId);
const h = harness(captured.pending.params.sessionId);
h.projector.tool(nativeEvent(captured.pending));
h.projector.captureSemanticReceipt()!(captured.authoritativeReceipt);
h.projector.tool(nativeEvent(captured.completed));
const output = captured.completed.params.update.rawOutput;
expect(output.content).toBe(output.contents.map((block: { text: string }) => block.text).join(""));
expect(output.detailedContent).toBe(output.content);
expect(JSON.parse(output.contents[0].text)).toMatchObject({ accepted: false });
expect(details(h.events.at(-1)!)).toMatchObject({ stage: "tool", status: "completed",
semanticOperationId: captured.authoritativeReceipt.operationId,
semanticCallIdentitySha256: captured.authoritativeReceipt.callIdentitySha256,
semanticInputSha256: captured.authoritativeReceipt.inputSha256,
semanticResultSha256: captured.authoritativeReceipt.resultSha256, semanticOutcome: "returned" });
expect(JSON.stringify(h.events)).not.toContain("known-small-result");
});
it.each(["missing-authority", "altered-block", "different-input", "missing-contents", "duplicate-terminal"])("does not trust captured native output alone: %s", scenario => {
const value = structuredClone(captured), h = harness(value.pending.params.sessionId);
if (scenario === "different-input") value.pending.params.update.rawInput = { changed: true };
if (scenario === "altered-block") value.completed.params.update.rawOutput.contents[0].text = '{"accepted":true}';
if (scenario === "missing-contents") delete value.completed.params.update.rawOutput.contents;
h.projector.tool(nativeEvent(value.pending));
if (scenario !== "missing-authority") h.projector.captureSemanticReceipt()!(value.authoritativeReceipt);
h.projector.tool(nativeEvent(value.completed));
if (scenario === "duplicate-terminal") h.projector.tool(nativeEvent(value.completed));
expect(details(h.events.at(-1)!)).not.toHaveProperty("semanticOperationId");
if (scenario === "duplicate-terminal") expect(details(h.events.at(-1)!)).toMatchObject({ stage: "evidence_incomplete", reason: "reused_semantic_lifecycle" });
});
});
@@ -9,11 +9,11 @@ import { cursorToolIdentity } from "./cursor-plan-tool-identity.js";
import { createCursorToolEvidence } from "./cursor-tool-evidence.js";
import { validateAcpxRichEvent } from "./profile-extensions.js";
const rawIds = ["native\u0000tool", "native\u007ftool", "safe-tool-1"];
const rawIds = ["native\u0000tool", "native\u007ftool", "native\u0085tool", "tool/1", "x".repeat(161), "safe-tool-1"];
// Synthetic protocol fixture, not a claim about the unretained paid wire bytes.
// Native permission deliberately omits rawInput, matching pinned Cursor's shape.
const peer = String.raw`
const ids=['native\u0000tool','native\u007ftool','safe-tool-1'];
const ids=['native\u0000tool','native\u007ftool','native\u0085tool','tool/1','x'.repeat(161),'safe-tool-1'];
const send=x=>process.stdout.write(JSON.stringify({jsonrpc:'2.0',...x})+'\n');
let promptId, index=0;
function next() {
@@ -87,7 +87,7 @@ it.each(["tool-first", "permission-first"])("real ACPX streams Cursor tool origi
}
await expect(turn.result).resolves.toMatchObject({ status: "completed" });
await drain;
expect(new Set(fields.map(row => row.toolCallId)).size).toBe(3);
expect(new Set(fields.map(row => row.toolCallId)).size).toBe(rawIds.length);
expect(fields.some(row => row.stage === "evidence_incomplete")).toBe(false);
} finally {
pending.forEach(value => value.release());
@@ -3,7 +3,7 @@ import type { AcpxExtensionInput } from "./profile-extensions.js";
// Match the sidecar's provider identity boundary before Rust's opaque tool ID.
export function cursorToolIdentity(value: string): string {
if (Buffer.byteLength(value) <= 240 && !/[\u0000-\u001f\u007f]/.test(value)) return value;
if (Buffer.byteLength(value) <= 240 && !/[\u0000-\u001f\u007f-\u009f]/.test(value)) return value;
return `acpx-tool-${createHash("sha256").update("paperclip.acpx.provider-identity.v1\0tool\0").update(value).digest("hex")}`;
}
@@ -25,13 +25,13 @@ it("waits for the original iterator event when permission arrives before the que
const s = setup(); const delivered = s.p.permission(request, "request", ["decline"]); expect(s.events).toHaveLength(0);
s.p.tool(initial); delivered!("reject_once"); expect(s.fields().map(x => x.stage)).toEqual(["tool", "permission_requested", "permission_delivered"]);
});
it.each(["tool-first", "permission-first"])("correlates opaque C0/DEL native identities in %s order without changing native requests", order => {
it.each(["tool-first", "permission-first"])("correlates opaque C0/C1/DEL native identities in %s order without changing native requests", order => {
const s = setup();
const rawIds = ["native\u0000tool", "native\u007ftool", "safe-tool-1"];
const rawIds = ["native\u0000tool", "native\u007ftool", "native\u0085tool", "tool/1", "x".repeat(161), "safe-tool-1"];
const expectedIds = rawIds.map(cursorToolIdentity);
expect(new Set(expectedIds).size).toBe(3);
expect(expectedIds.slice(0, 2).every(value => /^acpx-tool-[a-f0-9]{64}$/.test(value))).toBe(true);
expect(expectedIds[2]).toBe("safe-tool-1");
expect(new Set(expectedIds).size).toBe(rawIds.length);
expect(expectedIds.slice(0, 3).every(value => /^acpx-tool-[a-f0-9]{64}$/.test(value))).toBe(true);
expect(expectedIds.at(-1)).toBe("safe-tool-1");
for (const [index, toolCallId] of rawIds.entries()) {
const native = { sessionId: "session", inferredKind: "execute" as const, raw: {
sessionId: "session", toolCall: { toolCallId, kind: "execute" },
@@ -0,0 +1,60 @@
{
"pending": {
"jsonrpc": "2.0",
"method": "session/update",
"params": {
"sessionId": "captured-fixture-session",
"update": {
"sessionUpdate": "tool_call",
"toolCallId": "fixture-tool",
"title": "receipt-receipt_fixture",
"kind": "other",
"status": "pending",
"rawInput": {}
}
}
},
"completed": {
"jsonrpc": "2.0",
"method": "session/update",
"params": {
"sessionId": "captured-fixture-session",
"update": {
"sessionUpdate": "tool_call_update",
"toolCallId": "fixture-tool",
"status": "completed",
"content": [
{
"type": "content",
"content": {
"type": "text",
"text": "{\"fixture\":\"known-small-result\",\"accepted\":false}{\"schema\":\"paperclip.semantic_tool_receipt.v1\",\"operationId\":\"receipt_fixture\",\"callIdentitySha256\":\"8880622e75a82af5d9b588823af9d706f2bf3a04dce96ee9aab2fa4a91c55508\",\"inputSha256\":\"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a\",\"resultSha256\":\"b325c663fc4bb7617a745373367ca8cbc7a3445f1b2fdb3a2a151da39f73553c\",\"outcome\":\"returned\"}"
}
}
],
"rawOutput": {
"content": "{\"fixture\":\"known-small-result\",\"accepted\":false}{\"schema\":\"paperclip.semantic_tool_receipt.v1\",\"operationId\":\"receipt_fixture\",\"callIdentitySha256\":\"8880622e75a82af5d9b588823af9d706f2bf3a04dce96ee9aab2fa4a91c55508\",\"inputSha256\":\"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a\",\"resultSha256\":\"b325c663fc4bb7617a745373367ca8cbc7a3445f1b2fdb3a2a151da39f73553c\",\"outcome\":\"returned\"}",
"detailedContent": "{\"fixture\":\"known-small-result\",\"accepted\":false}{\"schema\":\"paperclip.semantic_tool_receipt.v1\",\"operationId\":\"receipt_fixture\",\"callIdentitySha256\":\"8880622e75a82af5d9b588823af9d706f2bf3a04dce96ee9aab2fa4a91c55508\",\"inputSha256\":\"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a\",\"resultSha256\":\"b325c663fc4bb7617a745373367ca8cbc7a3445f1b2fdb3a2a151da39f73553c\",\"outcome\":\"returned\"}",
"contents": [
{
"type": "text",
"text": "{\"fixture\":\"known-small-result\",\"accepted\":false}"
},
{
"type": "text",
"text": "{\"schema\":\"paperclip.semantic_tool_receipt.v1\",\"operationId\":\"receipt_fixture\",\"callIdentitySha256\":\"8880622e75a82af5d9b588823af9d706f2bf3a04dce96ee9aab2fa4a91c55508\",\"inputSha256\":\"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a\",\"resultSha256\":\"b325c663fc4bb7617a745373367ca8cbc7a3445f1b2fdb3a2a151da39f73553c\",\"outcome\":\"returned\"}"
}
]
}
}
}
},
"authoritativeReceipt": {
"schema": "paperclip.semantic_tool_receipt.v1",
"operationId": "receipt_fixture",
"callIdentitySha256": "8880622e75a82af5d9b588823af9d706f2bf3a04dce96ee9aab2fa4a91c55508",
"inputSha256": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
"resultSha256": "b325c663fc4bb7617a745373367ca8cbc7a3445f1b2fdb3a2a151da39f73553c",
"outcome": "returned"
}
}
@@ -0,0 +1,49 @@
{
"schema": "paperclip.native-mcp-capture-provenance.v1",
"nativeVersion": "1.0.88",
"nativeExecutableSha256": "a9ff8babb10b7e443182ae96a8bc50a9c826ef1c773e1344c396eb5bf7f512c3",
"platform": "darwin-arm64",
"sourceCommit": "cd2bd6f755a7685320766a71093320d7221172e9",
"captureKind": "real pinned native executable with deterministic loopback model and MCP server",
"paidProviderCalls": 0,
"credentialReads": false,
"qualification": false,
"scope": "One small successful two-text-block MCP result, original accepted=false; not native proof for every size/error encoding.",
"wireIndices": [
9,
11
],
"sanitization": [
"Replaced only params.sessionId in each selected frame with captured-fixture-session.",
"Excluded all other wire frames and model/configuration discovery.",
"Selected update objects and rawOutput preserved exactly.",
"authoritativeReceipt copied from owned fixture carrier.json; it is not an authority inferred from native output."
],
"originalEvidence": [
{
"name": "stdout.json",
"sha256": "95deeefff204b8e8db9c46b882cdea6fbe9b5c0de68efb56b4e9359cd0987208"
},
{
"name": "carrier.json",
"sha256": "c4f4fc333dfbc5efe33b46435a88c3c8ceff74237083e6eb03ad16bb8e783807"
},
{
"name": "carrier-proof.json",
"sha256": "4ed85f32732c2995506e4ffd23e85f79f26388aac6d8f47efefb5258c7b71da5"
},
{
"name": "execution-plan.json",
"sha256": "ae4435b5ed2c23a27e2d9c47efe599e3720b800a66e4f9ef56886428f21ee998"
},
{
"name": "probe.py",
"sha256": "264d39c1f6c36dadcd343b406a9f3ec851b29a55208c08cc3f6e26640ebeadd7"
},
{
"name": "HANDOFF.json",
"sha256": "e9859debd7274b42f5e58c28cc025bb2e3a090bb583e71ad460f2f482f6c3dae"
}
],
"fixtureSha256": "4af7345f219463813b382e4c6863003f9f957e147a96dc9c197b97ecd3e20cfb"
}
@@ -64,7 +64,7 @@ export const QUALIFIED_ACPX_PROFILES: Readonly<
acpxVersion: QUALIFIED_ACPX_VERSION, agent: "cursor", agentProfileVersion: 10,
agentServerPackage: "cursor-agent", agentServerVersion: "2026.09.26-dd393fe",
agentRuntimePackage: null, agentRuntimeVersion: null,
commandDigest: "sha256:e4ffecec5a7d43252f7f79cd681df582492b41a40dd33c1ad1a8354a178ccddb",
commandDigest: "sha256:1df2a15b93bc3a14fa47fa3315344ba023fe2412048047cdc6f32096a6336564",
// Authenticated discovery has not established a qualification model. Never
// turn this empty declaration into a default; callers must select an ID.
qualificationModel: "", reportedModelId: "", permissionPolicy: "interactive",
@@ -75,7 +75,7 @@ export const QUALIFIED_ACPX_PROFILES: Readonly<
acpxVersion: QUALIFIED_ACPX_VERSION, agent: "copilot", agentProfileVersion: 10,
agentServerPackage: "@github/copilot", agentServerVersion: "1.0.88",
agentRuntimePackage: null, agentRuntimeVersion: null,
commandDigest: "sha256:2d501724197e00564fb8cd8ce7b0f951c32fb6c1d3bfdde2dd0531171e3db266",
commandDigest: "sha256:c6741d7e49cd1af4ecad9e07181ef96305bde48b56dce2958547f2f066666231",
// Authenticated discovery has not established a qualification model. Never
// turn this empty declaration into a default; callers must select an ID.
qualificationModel: "", reportedModelId: "", permissionPolicy: "interactive",
@@ -48,13 +48,22 @@ vi.mock("../control-plane/durable-prp-control-plane.js", async (importOriginal)
},
};
});
import { cursorToolIdentity, cursorToolExecutionId } from "../drivers/acpx/cursor-plan-tool-identity.js";
import { normalizeAcpxPermission } from "../drivers/acpx/acp-permission-adapter.js";
import { createCapabilityRunnerdCodexTransport } from "./runnerd-codex-transport.js";
import { makeDriver, WORKSPACE } from "../drivers/codex/codex-app-server-driver.test-support.js";
import type { PrpEvent } from "../protocol/replay-contract.js";
describe("permission and tool order through the actual transport and harness", () => {
it.each(["tool-first", "permission-first"])("preserves committed batch order with an unanswered permission: %s", async order => {
it.each(["tool-first", "permission-first"].flatMap(order => ["native-tool-7", "native\u0080tool", "native\u0085tool", "native\u009ftool", "tool/1", "x".repeat(161)].map(rawId => ({ order, rawId }))))("preserves committed batch order and native identity with an unanswered permission: $order $rawId", async ({ order, rawId }) => {
const toolCallId = cursorToolIdentity(rawId);
const executionId = cursorToolExecutionId(rawId);
const normalized = normalizeAcpxPermission({ sessionId: "provider-session-1", inferredKind: "execute", raw: {
sessionId: "provider-session-1", toolCall: { toolCallId: rawId, kind: "execute" },
options: [{ kind: "reject_once", optionId: "original-native-denial", name: "Deny" }],
} }, { provider: "cursor" });
expect(normalized.toolCallId).toBe(toolCallId);
const root = mkdtempSync(join(tmpdir(), "permission-ordering-"));
const binary = join(root, "unexecuted-runner");
writeFileSync(binary, "not executable: process boundary is mocked");
@@ -75,14 +84,14 @@ describe("permission and tool order through the actual transport and harness", (
if (seen.some(e => e.eventType === "runtime_request.created") && seen.some(e => e.eventType === "tool.execution.started")) break;
} })();
const tool = () => peer.current!.emit("tool.execution.started", {
schema: "paperclip.tool.execution.v1", executionId: "native-tool-7", transport: "builtin", namespace: null,
schema: "paperclip.tool.execution.v1", executionId, transport: "builtin", namespace: null,
name: "Run command", operation: "execute", target: null, status: "running", readOnly: false,
inputUpdated: true, output: null, outputBytes: 0, outputTruncated: false, outputDigest: null,
durationMs: null, exitCode: null, progress: [],
});
const permission = () => peer.current!.emit("runtime_request.created", { request: {
requestId: "permission-7", type: "permission", requestKind: "permission_approval",
itemId: "opaque-item-7", prompt: "Run command", details: { toolCallId: "native-tool-7" },
itemId: "opaque-item-7", prompt: "Run command", details: { toolCallId: normalized.toolCallId },
choices: [{ key: "decline", label: "Deny" }, { key: "cancel", label: "Cancel" }],
origin: { adapter: "acpx-runtime-sidecar", provider: "cursor", method: "session/request_permission" },
} });
@@ -92,8 +101,12 @@ describe("permission and tool order through the actual transport and harness", (
await vi.waitFor(() => expect(seen.filter(e => ["tool.execution.started", "runtime_request.created"].includes(e.eventType))).toHaveLength(2), { timeout: 1_000, interval: 5 });
const nativeOrder = peer.current!.store.state.committedEvents.filter(e => ["tool.execution.started", "runtime_request.created"].includes(e.eventType)).map(e => e.eventType);
const projectedOrder = seen.filter(e => ["tool.execution.started", "runtime_request.created"].includes(e.eventType)).map(e => e.eventType);
expect(session.pendingRuntimeRequests!()).toMatchObject([{ requestId: "permission-7", turnId, status: "pending" }]);
expect(session.pendingRuntimeRequests!()).toMatchObject([{ requestId: "permission-7", turnId, status: "pending", details: { toolCallId } }]);
expect(projectedOrder).toEqual(nativeOrder);
// Permission details retain bounded native identity; activity retains
// Rust's opaque execution ID. Their deterministic join is intentional.
expect(seen.find(e => e.eventType === "tool.execution.started")?.payload).toMatchObject({ executionId });
if (rawId === "tool/1" || rawId.length === 161) expect(executionId).not.toBe(toolCallId);
} finally {
// This test owns no actual process or database. Retire the mocked peer
// and its controller timer without inventing a provider terminal event.
@@ -1,5 +1,5 @@
{
"commandDigest": "sha256:2d501724197e00564fb8cd8ce7b0f951c32fb6c1d3bfdde2dd0531171e3db266",
"commandDigest": "sha256:c6741d7e49cd1af4ecad9e07181ef96305bde48b56dce2958547f2f066666231",
"declaration": {
"schema": "paperclip.acpx_profile_declaration.v1",
"agent": "copilot",
@@ -20,11 +20,11 @@
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
"permissionContextContract": "copilot-edit-permission-context-v1",
"permissionContextSourceSha256": "fe1bb611bea289d81b1c7c973fbb01b93de133a7caa8fe78b7ff4ee1f485b224",
"permissionAdapterSourceSha256": "cef0777f90837bc87748de1dff5b17ab03b89e7e5df115037e82318baf4e02ff",
"permissionAdapterSourceSha256": "49421a779c76e286ce65d75052026eab9630b1995af9ccf045a2d43d284633ce",
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
"permissionRedactionSourceSha256": "687270d7bbe3dccce6c2fed8664e76cda724b43e545cb153b9f54b018de52927",
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
"permissionIdentitySourceSha256": "a23ea952e6d760b2692c85c4d5bf0c620682ff2efb62ac5c2e3af0cf0d95eafc",
"permissionIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v1",
"semanticReceiptSourceSha256": "3d3d0fe5ce0e4dbfc78cf95b1bfcff4d4b3760c548e74c6c12db5bcacdd46ae2",
"semanticBridgeSourceSha256": "ab8f872344b2beb50e3f9970e0631c451a5493e66d259582ecc9a214624269b5",
@@ -1,5 +1,5 @@
{
"commandDigest": "sha256:e4ffecec5a7d43252f7f79cd681df582492b41a40dd33c1ad1a8354a178ccddb",
"commandDigest": "sha256:1df2a15b93bc3a14fa47fa3315344ba023fe2412048047cdc6f32096a6336564",
"declaration": {
"schema": "paperclip.acpx_profile_declaration.v1",
"agent": "cursor",
@@ -52,8 +52,8 @@
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
"nativePlanToolIdentity": "request-item-id-bound-lifecycle-v1",
"nativePermissionToolIdentity": "opaque-native-tool-id-sha256-v1",
"toolIdentitySourceSha256": "a23ea952e6d760b2692c85c4d5bf0c620682ff2efb62ac5c2e3af0cf0d95eafc",
"permissionAdapterSourceSha256": "cef0777f90837bc87748de1dff5b17ab03b89e7e5df115037e82318baf4e02ff",
"toolIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
"permissionAdapterSourceSha256": "49421a779c76e286ce65d75052026eab9630b1995af9ccf045a2d43d284633ce",
"toolEvidenceSourceSha256": "8310308ed9605415cb28b7c350d83defef68e90cd30bc2f0491d3d09825e8cd9"
}
}
@@ -94,7 +94,8 @@ describe("accepted Cursor plan passive-wait authority", () => {
const facts = fixture();
expect(nativeCursorPlanWaitFromFacts(facts)).not.toBeNull();
const current = resolveQualifiedAcpxProfile("cursor", "gpt-5.6-luna[context=272k,reasoning=medium,fast=false]");
const next = { ...current, agentProfileVersion: 10, commandDigest: `sha256:${"b".repeat(64)}` } satisfies typeof current;
// Model a future catalog revision without tying this test to today's version.
const next = { ...current, agentProfileVersion: (current.agentProfileVersion + 1) as typeof current.agentProfileVersion, commandDigest: `sha256:${"b".repeat(64)}` } satisfies typeof current;
expect(next.agentProfileVersion).toBeGreaterThan(current.agentProfileVersion);
const resolver = vi.spyOn(runner, "resolveQualifiedAcpxProfile").mockReturnValue(next);
try { expect(nativeCursorPlanWaitFromFacts(facts)).toBeNull(); }