mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 02:26:12 +02:00
fix(runner): align Cursor control IDs and retain native receipt evidence
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
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" },
|
||||
|
||||
+60
@@ -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"
|
||||
}
|
||||
}
|
||||
+49
@@ -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",
|
||||
|
||||
+3
-3
@@ -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(); }
|
||||
|
||||
Reference in new issue
Block a user