fix(runner): commit receipts only for admitted sidecar frames

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-30 23:34:44 -05:00
1 parent ffdb1bce20
commit 450135c914
10 files changed
+126 -20

No files matched your search

@@ -11,7 +11,11 @@ failed even though its owned processes, sandbox and IPC retired cleanly.
Receipt v2 preserves the original `inputSha256` and separately records
`normalizedInputSha256` at the same invocation's actual validated forwarding
boundary. The capture commits only after that emission succeeds; absent or
invalid capture remains null. Native result matching still requires the exact
invalid capture remains null. The sidecar rejects an oversized frame or semantic
body that its Unicode encoder would change before committing the digest.
Rejected frames consume no stream sequence. Writable backpressure still means
the bytes were accepted into the stream buffer; it does not prove semantic
acceptance. Native result matching still requires the exact
invocation-owned receipt and all original call/input/result hashes. Acceptance
must independently match the normalized digest and the unique proposed/accepted
result bodies. Transport return alone never proves acceptance. The server
@@ -4,6 +4,8 @@ Current source checkpoint (2026-10-01): **Cursor v10, Copilot v11 and Pi v10
remain unqualified**. Copilot receipt v2 distinguishes original provider arguments
from the strictly validated input actually forwarded for completion. A same-call
capture binds both hashes without reconstructing omitted defaults in the grader.
Oversized frames and semantic bodies changed by Unicode repair fail before
capture commitment, and dropped frames do not consume stream sequence numbers.
The server's JWT heuristic now preserves only the fixed schema literal in a
validated semantic-receipt notice. Native call/result correlation and independent
canonical acceptance remain required. The failed v10 Daytona attempt is retained;
@@ -223,7 +223,7 @@ impl AcpxProviderDescriptor {
"1.0.88",
None,
None,
"sha256:e3057abcc2e6eb0db3a5c683a15e32159ac3caa78ba3d4c2a5d9221929e867dd",
"sha256:d56589c43437277b527ed61155de7d882e3ae5316d67502d8b98a0d435f69f99",
),
"grok" => (
"grok-4.7",
@@ -2812,7 +2812,7 @@ mod tests {
"copilot",
"@github/copilot",
"1.0.88",
"sha256:e3057abcc2e6eb0db3a5c683a15e32159ac3caa78ba3d4c2a5d9221929e867dd",
"sha256:d56589c43437277b527ed61155de7d882e3ae5316d67502d8b98a0d435f69f99",
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:e3057abcc2e6eb0db3a5c683a15e32159ac3caa78ba3d4c2a5d9221929e867dd";
const PROFILE_DIGEST = "sha256:d56589c43437277b527ed61155de7d882e3ae5316d67502d8b98a0d435f69f99";
/** 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 } = {}) {
@@ -15,7 +15,7 @@ import { deliverAcpxResponse } from "../drivers/acpx/response-delivery.js";
import { normalizeAcpxPermission } from "../drivers/acpx/acp-permission-adapter.js";
import { ACPX_CAPABILITY_PROFILES } from "../drivers/acpx/capability-profiles.js";
import { resolveQualifiedAcpxProfile } from "../drivers/acpx/qualified-profiles.js";
import { ACPX_SIDECAR_PROTOCOL_VERSION } from "../drivers/acpx/sidecar-protocol.js";
import { ACPX_SIDECAR_PROTOCOL_VERSION, ACPX_SIDECAR_MAX_FRAME_BYTES, stringifyAcpxSidecarFrame } 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";
@@ -280,6 +280,67 @@ describe("qualified ACPX runtime sidecar", () => {
expect(emitted).toEqual([]);
});
it("rejects normalized/envelope expansion at the actual frame writer before committing a receipt", async () => {
const raw = { reportedWorkDisposition: "done", summary: "Complete", evidence: [{ ref: "" }], verification: [],
completionClaim: { contractRevision: "1", objectiveSatisfied: true, criteria: [], remainingWork: [] } };
const request = { jsonrpc: "2.0", id: 3, method: "tools/call", params: { name: "paperclip_finish", arguments: raw } };
raw.evidence[0]!.ref = "x".repeat(ACPX_SIDECAR_MAX_FRAME_BYTES - 1 - Buffer.byteLength(JSON.stringify(request)));
expect(Buffer.byteLength(JSON.stringify(request))).toBe(ACPX_SIDECAR_MAX_FRAME_BYTES - 1);
const validation = validatePrpStructuredRunResult(raw);
if (!validation.ok) throw new Error("Invalid fixture");
expect(Buffer.byteLength(JSON.stringify(validation.result))).toBeLessThanOrEqual(ACPX_SIDECAR_MAX_FRAME_BYTES);
const wire = loadActualSidecarWriter();
const tools = new Map<string, unknown>(), emitted: unknown[] = [];
const handler = loadWaitForTool({ tools, emitted, validate: validatePrpStructuredRunResult, emit: wire.emit });
const bridge = await startRunnerToolBridge({ handler, captureSemanticReceipt: () => () => {} });
try {
const body = await (await fetch(bridge.url, { method: "POST", headers: { Authorization: `Bearer ${bridge.secret}`, "Content-Type": "application/json" },
body: JSON.stringify(request) })).json();
expect(body.result.isError).toBe(true);
expect(body.result.content[0].text).toBe("ACPX semantic tool call exceeds the sidecar frame limit");
expect(readNativeSemanticReceipt({ contents: body.result.content })).toMatchObject({ outcome: "error", normalizedInputSha256: null });
expect(tools.size).toBe(0);
expect(emitted).toEqual([]);
expect(wire.writes).toEqual([]);
expect(wire.errors).toEqual(["[paperclip-acpx-sidecar] output_frame_too_large\n"]);
expect(wire.sequence()).toBe(0);
expect(wire.emit("runtime.diagnostic", { code: "bounded", message: "rejected" })).toBe(true);
expect(JSON.parse(wire.writes[0]!)).toMatchObject({ sequence: 1, eventType: "runtime.diagnostic" });
} finally { await bridge.close(); }
});
it.each(["backpressure", "throws"])("uses actual writer acceptance, preserving %s semantics", async mode => {
const wire = loadActualSidecarWriter(mode);
const tools = new Map<string, any>(), emitted: any[] = [], commit = vi.fn(), capture = vi.fn((_input: unknown) => commit);
const raw = { reportedWorkDisposition: "done", summary: "Complete", evidence: [], verification: [],
completionClaim: { contractRevision: "1", objectiveSatisfied: true, criteria: [], remainingWork: [] } };
const handler = loadWaitForTool({ tools, emitted, validate: validatePrpStructuredRunResult, emit: wire.emit });
const pending = handler({ tool: "paperclip_finish", callId: "3", arguments: raw, signal: new AbortController().signal, captureNormalizedInput: capture });
if (mode === "throws") {
await expect(pending).rejects.toThrow("fixture write failed");
expect(commit).not.toHaveBeenCalled(); expect(tools.size).toBe(0); expect(wire.sequence()).toBe(0);
} else {
expect(commit).toHaveBeenCalledOnce(); expect(tools.has("3")).toBe(true); expect(wire.sequence()).toBe(1);
expect(JSON.parse(wire.writes[0]!).payload.input).toEqual(capture.mock.calls[0]![0]);
tools.get("3").settle({ accepted: true }); tools.get("3").cleanup();
await expect(pending).resolves.toEqual({ accepted: true });
}
});
it.each(["value", "property", "collision"])("rejects semantic %s Unicode rewriting before capture or write", async kind => {
const raw = { reportedWorkDisposition: "done", summary: kind === "value" ? "\ud800" : "Complete", evidence: [{ ref: "fixture" } as Record<string, string>], verification: [],
completionClaim: { contractRevision: "1", objectiveSatisfied: true, criteria: [], remainingWork: [] } };
if (kind !== "value") raw.evidence[0]!["\udc00"] = "one";
if (kind === "collision") raw.evidence[0]!["\ufffd"] = "two";
expect(validatePrpStructuredRunResult(raw).ok).toBe(true);
const wire = loadActualSidecarWriter(), tools = new Map<string, unknown>(), emitted: unknown[] = [], capture = vi.fn(() => vi.fn());
const handler = loadWaitForTool({ tools, emitted, validate: validatePrpStructuredRunResult, emit: wire.emit });
await expect(handler({ tool: "paperclip_finish", callId: "3", arguments: raw, signal: new AbortController().signal,
captureNormalizedInput: capture })).rejects.toThrow("cannot be encoded without changing its identity");
expect(capture).not.toHaveBeenCalled(); expect(tools.size).toBe(0); expect(emitted).toEqual([]);
expect(wire.writes).toEqual([]); expect(wire.sequence()).toBe(0);
});
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"));
@@ -1062,7 +1123,7 @@ function loadWaitForTool(input: {
tools: Map<string, unknown>;
emitted: unknown[];
validate?: typeof validatePrpStructuredRunResult;
emit?: () => void;
emit?: (eventType: string, payload: unknown) => boolean;
}): (call: RunnerToolCall) => Promise<unknown> {
const source = readFileSync(
fileURLToPath(new URL("./acpx-runtime-sidecar.ts", import.meta.url)),
@@ -1079,14 +1140,14 @@ function loadWaitForTool(input: {
);
const factory = new Function(
"boundedIdentity", "tools", "turnId", "emit", "PRP_COMPLETION_TOOL_NAME",
"PRP_BLOCK_TOOL_NAME", "validatePrpStructuredRunResult", "boundedSidecarValue", "record", "MAX_PENDING_TOOLS",
"PRP_BLOCK_TOOL_NAME", "validatePrpStructuredRunResult", "boundedSidecarValue", "record", "MAX_PENDING_TOOLS", "stringifyAcpxSidecarFrame",
`return (${functionSource});`,
);
return factory(
(value: string) => value,
input.tools,
"test-turn",
(_eventType: string, payload: unknown) => { input.emit?.(); input.emitted.push(payload); },
(eventType: string, payload: unknown) => { const accepted = input.emit?.(eventType, payload) ?? true; if (accepted) input.emitted.push(payload); return accepted; },
"paperclip_finish",
"paperclip_block",
input.validate ?? ((argumentsValue: unknown) => ({
@@ -1096,5 +1157,26 @@ function loadWaitForTool(input: {
(value: unknown) => value,
(value: unknown) => value,
512,
stringifyAcpxSidecarFrame,
);
}
/** Actual producer and encoder, with only the destination stream replaced. */
function loadActualSidecarWriter(mode: "normal" | "backpressure" | "throws" = "normal"): {
emit: (eventType: string, payload: unknown) => boolean; sequence: () => number; writes: string[]; errors: string[];
} {
const source = readFileSync(new URL("./acpx-runtime-sidecar.ts", import.meta.url), "utf8");
const emitStart = source.indexOf("function emit("), emitEnd = source.indexOf("\nfunction diagnostic", emitStart);
const writeStart = source.indexOf("function writeFrame("), writeEnd = source.indexOf("\nfunction requireHost", writeStart);
if (emitStart < 0 || emitEnd < 0 || writeStart < 0 || writeEnd < 0) throw new Error("Actual sidecar writer source not found");
const writes: string[] = [], errors: string[] = [];
const process = { stdout: { write: (line: string) => { if (mode === "throws") throw new Error("fixture write failed"); writes.push(line); return mode !== "backpressure"; } },
stderr: { write: (line: string) => { errors.push(line); return true; } } };
const writer = new Function("process", "stringifyAcpxSidecarFrame", "ACPX_SIDECAR_PROTOCOL_VERSION", "ACPX_SIDECAR_MAX_FRAME_BYTES", `
let sequence=0; const runId="test-run", turnId="test-turn";
${stripTypeScriptTypes(source.slice(emitStart, emitEnd))}
${stripTypeScriptTypes(source.slice(writeStart, writeEnd))}
return { emit, sequence: () => sequence };
`)(process, stringifyAcpxSidecarFrame, ACPX_SIDECAR_PROTOCOL_VERSION, ACPX_SIDECAR_MAX_FRAME_BYTES);
return { ...writer, writes, errors };
}
@@ -711,6 +711,15 @@ async function waitForTool(call: RunnerToolCall): Promise<unknown> {
"ACPX semantic result disposition does not match its terminal operation",
);
}
// Semantic result identity must survive the wire encoder unchanged. Its
// display-text Unicode repair is unsuitable for authenticated result data.
try {
if (stringifyAcpxSidecarFrame(validation.result) !== JSON.stringify(validation.result)) {
throw new Error("Semantic input changes during encoding");
}
} catch {
throw new Error("ACPX semantic result cannot be encoded without changing its identity");
}
// The authenticated runner bridge must admit this built-in invocation
// before the provider sees a result. Keep the call pending until runnerd
// sends tool.resolve after the server's completion feedback accepts it.
@@ -718,7 +727,7 @@ async function waitForTool(call: RunnerToolCall): Promise<unknown> {
// local semantic_result here would let an invalid review handoff appear
// accepted before the server has checked it.
const commitNormalizedInput = call.captureNormalizedInput?.(validation.result);
emit(
const forwarded = emit(
"runtime.tool_called",
{
callId,
@@ -727,6 +736,7 @@ async function waitForTool(call: RunnerToolCall): Promise<unknown> {
},
activeTurnId,
);
if (!forwarded) throw new Error("ACPX semantic tool call exceeds the sidecar frame limit");
commitNormalizedInput?.();
return await new Promise((settle, reject) => {
const abort = () => {
@@ -1327,18 +1337,21 @@ function emit(
eventType: AcpxSidecarEvent["eventType"],
payload: Record<string, unknown>,
eventTurnId: string | null = turnId,
): void {
): boolean {
if (sequence >= Number.MAX_SAFE_INTEGER) {
throw new Error("ACPX sidecar event sequence exhausted");
}
writeFrame({
const accepted = writeFrame({
protocolVersion: ACPX_SIDECAR_PROTOCOL_VERSION,
sequence: ++sequence,
sequence: sequence + 1,
eventType,
runId,
turnId: eventTurnId,
payload,
});
// A dropped frame never entered the stream and must not consume an identity.
if (accepted) sequence++;
return accepted;
}
function diagnostic(code: string, message: string): void {
@@ -1362,13 +1375,16 @@ function response(
});
}
function writeFrame(value: AcpxSidecarEvent | AcpxSidecarResponse): void {
function writeFrame(value: AcpxSidecarEvent | AcpxSidecarResponse): boolean {
const line = stringifyAcpxSidecarFrame(value);
if (Buffer.byteLength(line) > ACPX_SIDECAR_MAX_FRAME_BYTES) {
process.stderr.write("[paperclip-acpx-sidecar] output_frame_too_large\n");
return;
return false;
}
process.stdout.write(`${line}\n`);
// Writable.write(false) accepted the bytes into its queue; it signals
// backpressure, not a rejected frame. A synchronous write error still throws.
return true;
}
function requireHost(
@@ -24,6 +24,7 @@ const SEMANTIC_RECEIPT_SOURCES = [
["../runner-tool-bridge.ts", "semanticBridgeSourceSha256"],
["copilot-tool-evidence.ts", "toolEvidenceSourceSha256"],
["../../cli/acpx-runtime-sidecar.ts", "semanticSidecarSourceSha256"],
["sidecar-protocol.ts", "semanticSidecarProtocolSourceSha256"],
["codex-acpx-driver.ts", "semanticDirectDriverSourceSha256"],
["../../protocol/replay-contract.ts", "semanticValidationSourceSha256"],
["../../protocol/result-normalization.ts", "semanticNormalizationSourceSha256"],
@@ -75,7 +75,7 @@ export const QUALIFIED_ACPX_PROFILES: Readonly<
acpxVersion: QUALIFIED_ACPX_VERSION, agent: "copilot", agentProfileVersion: 11,
agentServerPackage: "@github/copilot", agentServerVersion: "1.0.88",
agentRuntimePackage: null, agentRuntimeVersion: null,
commandDigest: "sha256:e3057abcc2e6eb0db3a5c683a15e32159ac3caa78ba3d4c2a5d9221929e867dd",
commandDigest: "sha256:d56589c43437277b527ed61155de7d882e3ae5316d67502d8b98a0d435f69f99",
// 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",
@@ -758,8 +758,8 @@ describe("ACPX runtime host", () => {
const receipt = readNativeSemanticReceipt({ contents: body.result.content });
expect(receipt).not.toBeNull();
expect(events).toHaveLength(1);
expect(events[0]!.payload).toMatchObject({ category: "paperclip_semantic_tool_receipt_v1", provenance: { sessionId: "backend-1", turnId: "turn-1" },
details: expect.arrayContaining([{ name: "callIdentitySha256", value: receipt!.callIdentitySha256 }, { name: "resultSha256", value: receipt!.resultSha256 }]) });
expect(events[0]!.payload).toMatchObject({ category: "paperclip_semantic_tool_receipt_v2", provenance: { sessionId: "backend-1", turnId: "turn-1" },
details: expect.arrayContaining([{ name: "schema", value: "paperclip.semantic_tool_receipt.v2" }, { name: "normalizedInputSha256", value: "null" }, { name: "callIdentitySha256", value: receipt!.callIdentitySha256 }, { name: "resultSha256", value: receipt!.resultSha256 }]) });
} finally { await host.close({ reason: "receipt forwarding verified" }); }
await expect(fetch(bridge!.url)).rejects.toThrow();
});
@@ -1,5 +1,5 @@
{
"commandDigest": "sha256:e3057abcc2e6eb0db3a5c683a15e32159ac3caa78ba3d4c2a5d9221929e867dd",
"commandDigest": "sha256:d56589c43437277b527ed61155de7d882e3ae5316d67502d8b98a0d435f69f99",
"declaration": {
"schema": "paperclip.acpx_profile_declaration.v1",
"agent": "copilot",
@@ -30,12 +30,13 @@
"semanticBridgeSourceSha256": "8a43ebafb7a85dd1714ed6945670472666dcb63e8af32b4a74644163fa0e370d",
"toolEvidenceSourceSha256": "4b517eed447615b3cc820af348ccc360978175149caa0bbc838d1f78332abe75",
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
"semanticSidecarSourceSha256": "6c7dedca55f0f031570349b010ede1cd8015c25e9a52afac01ce9de8beb966c2",
"semanticSidecarSourceSha256": "a261497560840da66613a5f4a3dafeba05e53d3a8647c180c9604b3cf983dc23",
"semanticDirectDriverSourceSha256": "31e36917ee3592fa6d8f4b86632129fdffae20c45aed2e0e367ccf523e064f88",
"semanticValidationSourceSha256": "039fca52266fe21ba74ca64660d21228314d8ed737b19ba67941bfb25086e4d5",
"semanticNormalizationSourceSha256": "b1ce59251bf9c671f66e0e5173646190b9a2d1bfc5bdb7fb123824213986b92b",
"semanticCompletionContractSourceSha256": "00a684f9aea87f68eabf0974681c52461bc308631a1252a89c4ed087d7158b28",
"semanticValidatorsSourceSha256": "950b49b233d8dd62ff858398becfc55f0a181160cb8a434d9221b5c17816867c",
"semanticSchemaBundleSourceSha256": "bb55e18c5563bf5ec226e301c3399229304f295e986865e96554f193535368f2"
"semanticSchemaBundleSourceSha256": "bb55e18c5563bf5ec226e301c3399229304f295e986865e96554f193535368f2",
"semanticSidecarProtocolSourceSha256": "165cad76f224d60f0dc7928736d47a16787eaa8ab9e4c2222488da5fc8ad632e"
}
}