fix(runner): require receiver acknowledgment for semantic receipt

This commit is contained in:
Dotta committed 2026-10-01 00:14:49 -05:00
1 parent 2e4e06d7f0
commit 22c78242a4
15 files changed
+335 -51

No files matched your search

+24 -22
View File
@@ -1,28 +1,30 @@
# Copilot 1.0.88 rich ACP capability audit
Current source candidate (2026-10-01): **Copilot profile v11 is unqualified**.
The authenticated Daytona v10 attempt exposed two independent receipt-boundary
failures. Generic JWT redaction removed the public receipt schema name from a
notice detail. Separately, a legitimate completion omitted optional fields that
the strict result validator fills before forwarding; its original argument hash
therefore differed from the proposed canonical result. The failed attempt remains
failed even though its owned processes, sandbox and IPC retired cleanly.
Current source candidate (2026-10-01): **Copilot profile v12 is unqualified**.
Receipt v2 preserves the original `inputSha256` and separately captures
`normalizedInputSha256` from the same invocation's validated outgoing body.
For the native sidecar, only a successful, correlated `tool.resolve` commits
that capture. A pipe write alone cannot attest delivery: Rust can reject an
otherwise valid frame at its smaller payload admission limit. Explicit errors,
cancellation, timeout, missing responses and stale or foreign responses leave
the capture null. A returned `accepted:false` witnesses delivery but still
rejects completion. The direct driver commits only after its proposal emission.
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. 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
preserves only the fixed schema literal inside a validated receipt notice;
adjacent credentials and JWT-shaped values remain redacted. Retained v10 warm
sessions are incompatible. Native executable bytes are unchanged; new runtime
packs and fresh local/Daytona qualification are required.
Delivery does not prove canonical equality or completion acceptance. The grader
must independently match the normalized digest with the unique proposed and
accepted result bodies; receiver sanitization does not relax that comparison.
The sidecar also rejects oversized frames or bodies its Unicode encoder would
change. Dropped frames consume no stream sequence; writable backpressure is
handled as an accepted buffer write, without claiming receiver admission.
The server preserves only the fixed schema literal inside a validated receipt
notice; adjacent credentials and JWT-shaped values remain redacted. The paid
v10 Daytona failure remains failed. The v11 build and offline CI evidence is
historical and does not qualify this corrected source. Retained v10 and v11
warm sessions are incompatible. Native executable bytes are unchanged; fresh
runtime packs and local/Daytona qualification are required. Tests exercise the
actual parsed sidecar command handler and the Rust subprocess transport,
including rejection of a 300 KiB payload before pending-tool admission.
Historical source candidate (2026-10-01): **Copilot profile v10 was unqualified**.
Admitted Paperclip MCP calls append a bounded `paperclip.semantic_tool_receipt.v1`
@@ -1,16 +1,21 @@
# Rich ACP integration and qualification report
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;
fresh v11 runtime packaging and local/Daytona evidence are still required.
The [Copilot inventory](runner-copilot-capabilities.md) records both boundaries.
Current source checkpoint (2026-10-01): **Cursor v10, Copilot v12 and Pi v10
remain unqualified**. Copilot receipt v2 distinguishes original provider
arguments from the validated outgoing completion input. The sidecar commits
the captured normalized digest only after its exact pending call receives a
successful, turn-bound `tool.resolve`. Pipe-write success alone is insufficient:
Rust may reject a frame at its smaller payload admission limit. Errors,
cancellation, timeout and stale responses cannot attest delivery.
Delivery remains separate from canonical equality and acceptance. The grader
still requires the normalized digest to match the unique proposed and accepted
result bodies. Oversized frames and Unicode repair fail before commitment;
dropped frames do not consume stream sequence numbers. The server preserves
only the fixed schema literal inside a validated semantic-receipt notice.
The failed v10 Daytona attempt and held v11 builds remain historical. Fresh
v12 runtime packaging and local/Daytona evidence are required. The
[Copilot inventory](runner-copilot-capabilities.md) records these boundaries.
Historical source checkpoint (2026-10-01): **Cursor v10, Copilot v10 and Pi v10
remain unqualified**. Cursor now shares the bounded native tool-ID mapping
@@ -223,7 +223,7 @@ impl AcpxProviderDescriptor {
"1.0.88",
None,
None,
"sha256:d56589c43437277b527ed61155de7d882e3ae5316d67502d8b98a0d435f69f99",
"sha256:48cecd8dc77a5533240fcf2f29d19be05380da4a79f8e5061480f94241db75a8",
),
"grok" => (
"grok-4.7",
@@ -2812,7 +2812,7 @@ mod tests {
"copilot",
"@github/copilot",
"1.0.88",
"sha256:d56589c43437277b527ed61155de7d882e3ae5316d67502d8b98a0d435f69f99",
"sha256:48cecd8dc77a5533240fcf2f29d19be05380da4a79f8e5061480f94241db75a8",
None,
None,
"explicit-model",
@@ -2914,6 +2914,13 @@ mod tests {
assert!(wrong_agent.validate(&context()).is_err());
}
if agent == "copilot" {
let mut previous_v11 = value.clone();
previous_v11["commandDigest"] = json!(
"sha256:d56589c43437277b527ed61155de7d882e3ae5316d67502d8b98a0d435f69f99"
);
let previous_v11: AcpxProviderDescriptor =
serde_json::from_value(previous_v11).unwrap();
assert!(previous_v11.validate(&context()).is_err());
let mut previous_v10 = value.clone();
previous_v10["commandDigest"] = json!(
"sha256:c6741d7e49cd1af4ecad9e07181ef96305bde48b56dce2958547f2f066666231"
@@ -25,6 +25,21 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
.find(|pair| pair[0] == "--profile-digest")
.map(|pair| pair[1].as_str())
.unwrap_or("sha256:1111111111111111111111111111111111111111111111111111111111111111");
// Only the receiver-admission fixture writes this task-owned command journal.
let mut admission_journal = if matches!(mode, "admission-tool" | "admission-tool-oversized") {
let path = args
.windows(2)
.find(|pair| pair[0] == "--journal")
.ok_or("admission journal is missing")?;
Some(
std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&path[1])?,
)
} else {
None
};
let stdin = io::stdin();
let mut stdout = io::stdout().lock();
let mut next_sequence = 1_u64;
@@ -39,6 +54,34 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
.get("command")
.and_then(Value::as_str)
.ok_or("request command is missing")?;
if let Some(journal) = admission_journal.as_mut() {
write_json(journal, &json!({"request":request}))?;
let response = if command == "tool.resolve" {
json!({"protocolVersion":GENERATED_ACPX_SIDECAR_PROTOCOL_VERSION,
"id":id,"ok":true,"result":{"resolved":
request["params"]["callId"] == "call-admission"
&& request["params"]["turnId"] == "turn-admission"
&& request["params"]["result"] == json!({"id":"issue-1"})
&& request["params"]["error"].is_null()}})
} else {
bootstrap_success(id, command, &request, mode, profile_digest)
};
write_json(&mut stdout, &response)?;
if command == "turn.start" {
let frame = json!({
"protocolVersion":GENERATED_ACPX_SIDECAR_PROTOCOL_VERSION,
"sequence":next_sequence,"eventType":"runtime.tool_called",
"runId":"run-1","turnId":request["params"]["turnId"],
"payload":{"callId":"call-admission","operationId":"issues.read",
"input":{"id":"issue-1","padding":"x".repeat(
if mode == "admission-tool-oversized" { 300 * 1024 } else { 8 })}},
});
write_json(journal, &json!({"emitted":frame}))?;
write_json(&mut stdout, &frame)?;
next_sequence += 1;
}
continue;
}
if mode == "goals" && command.starts_with("session.goal.") {
let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
match command {
@@ -227,3 +227,100 @@ fn rejects_non_utf8_directories_before_spawning() {
assert!(error.contains("must be valid UTF-8"), "{error}");
}
/// Exercise the subprocess transport, real event decoder/reducer and tool bridge.
/// The command journal distinguishes no admission from a late sidecar rejection.
fn check_tool_receiver_admission(oversized: bool) {
let root = std::env::temp_dir().join(format!("acpx-admission-{}", uuid::Uuid::new_v4()));
std::fs::create_dir(&root).unwrap();
let journal = root.join("commands.jsonl");
let mut fixture = config(if oversized {
"admission-tool-oversized"
} else {
"admission-tool"
});
fixture
.transport
.args
.extend(["--journal".to_owned(), journal.to_str().unwrap().to_owned()]);
let mut session = AcpxProviderSession::start(&fixture).unwrap();
session
.start_turn("turn-admission", "Read the issue", &std::env::temp_dir())
.unwrap();
let result = paperclip_runner_core::provider_bridge::ToolResult {
call_id: "call-admission".to_owned(),
operation_id: "issues.read".to_owned(),
result: json!({"id":"issue-1"}),
is_error: false,
};
if oversized {
let error = session
.poll_event(Duration::from_secs(1))
.unwrap_err()
.to_string();
assert!(error.contains("256 KiB admission limit"), "{error}");
assert!(!session.state().has_pending_tools());
assert!(session.state().pending_tool("call-admission").is_none());
// Rejection cannot be bypassed by supplying a matching callback result.
assert!(session.deliver_tool_result(&result).is_err());
} else {
let events = session.poll_event(Duration::from_secs(1)).unwrap().unwrap();
assert_eq!(events.len(), 1);
assert!(session.state().pending_tool("call-admission").is_some());
let mut foreign = result.clone();
foreign.call_id = "another-call".to_owned();
assert!(session.deliver_tool_result(&foreign).is_err());
assert!(session.state().pending_tool("call-admission").is_some());
session.deliver_tool_result(&result).unwrap();
assert!(!session.state().has_pending_tools());
assert!(session.deliver_tool_result(&result).is_err());
}
session.shutdown("admission test complete").unwrap();
let rows: Vec<serde_json::Value> = std::fs::read_to_string(&journal)
.unwrap()
.lines()
.map(|line| serde_json::from_str(line).unwrap())
.collect();
let emitted: Vec<_> = rows.iter().filter_map(|row| row.get("emitted")).collect();
assert_eq!(emitted.len(), 1);
let frame = emitted[0];
assert_eq!(frame["eventType"], "runtime.tool_called");
let payload_bytes = serde_json::to_vec(&frame["payload"]).unwrap().len();
let frame_bytes = serde_json::to_vec(frame).unwrap().len() + 1;
assert!(
frame_bytes < 1024 * 1024,
"must reach payload admission, not the line limit"
);
assert_eq!(payload_bytes > 256 * 1024, oversized);
let resolutions: Vec<_> = rows
.iter()
.filter_map(|row| row.get("request"))
.filter(|request| request["command"] == "tool.resolve")
.collect();
if oversized {
assert!(
resolutions.is_empty(),
"rejected event must never send tool.resolve"
);
} else {
assert_eq!(resolutions.len(), 1);
assert_eq!(
resolutions[0]["params"],
json!({
"callId":"call-admission","turnId":"turn-admission",
"result":{"id":"issue-1"},"error":null,
})
);
}
std::fs::remove_dir_all(root).unwrap();
}
#[test]
fn receiver_rejects_sub_megabyte_tool_event_before_pending_admission_or_resolution() {
check_tool_receiver_admission(true);
}
#[test]
fn receiver_admits_normal_tool_event_and_resolves_only_correlated_call_once() {
check_tool_receiver_admission(false);
}
@@ -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:d56589c43437277b527ed61155de7d882e3ae5316d67502d8b98a0d435f69f99";
const PROFILE_DIGEST = "sha256:48cecd8dc77a5533240fcf2f29d19be05380da4a79f8e5061480f94241db75a8";
/** 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 } = {}) {
@@ -563,6 +563,7 @@ describe("native backend factory", () => {
["copilot", "../../test/fixtures/copilot-profile-v7-identity.json"],
["copilot", "../../test/fixtures/copilot-profile-v9-identity.json"],
["copilot", "../../test/fixtures/copilot-profile-v10-identity.json"],
["copilot", "../../test/fixtures/copilot-profile-v11-identity.json"],
["pi", "../../test-fixtures/pi-acp/profile-v9-identity.json"],
] as const)("rejects the exact historical %s identity before runtime startup", (agent, path) => {
const historical = JSON.parse(readFileSync(new URL(path, import.meta.url), "utf8"));
@@ -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, ACPX_SIDECAR_MAX_FRAME_BYTES, stringifyAcpxSidecarFrame } from "../drivers/acpx/sidecar-protocol.js";
import { ACPX_SIDECAR_PROTOCOL_VERSION, ACPX_SIDECAR_MAX_FRAME_BYTES, stringifyAcpxSidecarFrame, parseAcpxSidecarRequest, record, text } 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";
@@ -23,6 +23,7 @@ import { createCursorToolEvidence } from "../drivers/acpx/cursor-tool-evidence.j
import { validateAcpxRichEvent } from "../drivers/acpx/profile-extensions.js";
import {
awaitSidecarCleanupWithin,
boundedIdentity,
closeActiveSidecarHostWithin,
closeSidecarHostForCommand,
combineSidecarAdmissionCleanups,
@@ -251,7 +252,7 @@ describe("qualified ACPX runtime sidecar", () => {
await vi.waitFor(() => expect(tools.has("3")).toBe(true));
expect(emitted).toEqual([{ callId: "3", operationId: "paperclip_finish", input: validated.result }]);
// The receipt hashes the actual emitted input, not a second normalization.
tools.get("3").settle({ accepted: true });
await loadActualToolCommands(tools).resolve({ callId: "3", turnId: "test-turn", result: { accepted: true }, error: null });
}
const body = await (await response).json();
const receipt = readNativeSemanticReceipt({ contents: body.result.content });
@@ -320,13 +321,70 @@ describe("qualified ACPX runtime sidecar", () => {
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(commit).not.toHaveBeenCalled(); 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 loadActualToolCommands(tools).resolve({ callId: "3", turnId: "test-turn", result: { accepted: true }, error: null });
await expect(pending).resolves.toEqual({ accepted: true });
expect(commit).toHaveBeenCalledOnce();
}
});
it.each(["success", "accepted-false", "error", "cancel", "timeout", "receiver-rejected"])(
"commits normalized delivery only after the actual tool.resolve handler succeeds: %s",
async mode => {
const raw = { reportedWorkDisposition: "done", summary: "Complete", evidence: [{ ref: mode === "receiver-rejected" ? "x".repeat(300 * 1024) : "fixture" }], verification: [],
completionClaim: { contractRevision: "1", objectiveSatisfied: true, criteria: [], remainingWork: [] } };
const validation = validatePrpStructuredRunResult(raw);
if (!validation.ok) throw new Error("Invalid fixture");
const wire = loadActualSidecarWriter(), tools = new Map<string, unknown>(), emitted: unknown[] = [], receipts: unknown[] = [];
const commands = loadActualToolCommands(tools);
const handler = loadWaitForTool({ tools, emitted, validate: validatePrpStructuredRunResult, emit: wire.emit });
const bridge = await startRunnerToolBridge({ handler, timeoutMs: 500, captureSemanticReceipt: () => receipt => receipts.push(receipt) });
const headers = { Authorization: `Bearer ${bridge.secret}`, "Content-Type": "application/json" };
const params = { callId: "3", turnId: "test-turn", result: { accepted: mode !== "accepted-false" }, error: null };
try {
const response = fetch(bridge.url, { method: "POST", headers,
body: JSON.stringify({ jsonrpc: "2.0", id: 3, method: "tools/call", params: { name: "paperclip_finish", arguments: raw } }) });
await vi.waitFor(() => expect(tools.has("3")).toBe(true), { interval: 5, timeout: 400 });
expect(wire.writes).toHaveLength(1);
expect(JSON.parse(wire.writes[0]!).payload.input).toEqual(validation.result);
expect(receipts).toEqual([]);
// These all enter the actual parsed command handler. None may consume
// the pending call or attest delivery for a different request/turn.
await expect(commands.resolve({ ...params, callId: "missing" })).rejects.toThrow("stale or unknown");
await expect(commands.resolve({ ...params, turnId: "foreign" })).rejects.toThrow("stale or unknown");
commands.setTurn("foreign");
await expect(commands.resolve(params)).rejects.toThrow("stale or unknown");
commands.setTurn("test-turn");
expect(tools.has("3")).toBe(true);
if (mode === "success" || mode === "accepted-false") {
await expect(commands.resolve(params)).resolves.toEqual({ resolved: true });
} else if (mode === "error") {
await expect(commands.resolve({ ...params, result: undefined, error: { message: "Receiver rejected completion" } })).resolves.toEqual({ resolved: true });
} else if (mode === "cancel") {
const cancelled = await fetch(bridge.url, { method: "POST", headers,
body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/cancelled", params: { requestId: 3 } }) });
expect(cancelled.status).toBe(202);
} else if (mode === "receiver-rejected") {
// Actual writer acceptance is not receiver admission. This frame is
// over Rust's payload bound; no correlated resolution arrives.
expect(Buffer.byteLength(JSON.stringify(JSON.parse(wire.writes[0]!).payload))).toBeGreaterThan(256 * 1024);
expect(Buffer.byteLength(wire.writes[0]!)).toBeLessThan(ACPX_SIDECAR_MAX_FRAME_BYTES);
}
const body = await (await response).json();
const delivered = mode === "success" || mode === "accepted-false";
const receipt = readNativeSemanticReceipt({ contents: body.result.content });
expect(receipt).toMatchObject({ normalizedInputSha256: delivered ? semanticInputSha256(validation.result) : null,
inputSha256: semanticInputSha256(raw), outcome: delivered ? "returned" : "error" });
expect(receipts).toEqual([receipt]);
expect(tools.size).toBe(0);
await expect(commands.resolve(params)).rejects.toThrow("stale or unknown");
expect(receipts).toEqual([receipt]);
} finally { await bridge.close(); }
},
);
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: [] } };
@@ -1180,3 +1238,24 @@ function loadActualSidecarWriter(mode: "normal" | "backpressure" | "throws" = "n
`)(process, stringifyAcpxSidecarFrame, ACPX_SIDECAR_PROTOCOL_VERSION, ACPX_SIDECAR_MAX_FRAME_BYTES);
return { ...writer, writes, errors };
}
/** Actual parsed command dispatcher; only the owned pending map/turn are supplied. */
function loadActualToolCommands(tools: Map<string, unknown>): {
resolve: (params: Record<string, unknown>) => Promise<Record<string, unknown>>;
setTurn: (turnId: string | null) => void;
} {
const source = readFileSync(new URL("./acpx-runtime-sidecar.ts", import.meta.url), "utf8");
const start = source.indexOf("async function dispatch("), end = source.indexOf("\nasync function pumpTurn(", start);
const safeStart = source.indexOf("function safeText("), safeEnd = source.indexOf("\nfunction safeMessage(", safeStart);
if (start < 0 || end < 0 || safeStart < 0 || safeEnd < 0) throw new Error("Actual sidecar command dispatcher not found");
const dispatcher = new Function("tools", "boundedIdentity", "record", "text", "parseAcpxSidecarRequest", "ACPX_SIDECAR_PROTOCOL_VERSION", `
let turnId="test-turn", requestId=0;
${stripTypeScriptTypes(source.slice(safeStart, safeEnd))}
${stripTypeScriptTypes(source.slice(start, end))}
return {
resolve: params => dispatch(parseAcpxSidecarRequest({ protocolVersion: ACPX_SIDECAR_PROTOCOL_VERSION, id: ++requestId, command: "tool.resolve", params })),
setTurn: value => { turnId=value; }
};
`)(tools, boundedIdentity, record, text, parseAcpxSidecarRequest, ACPX_SIDECAR_PROTOCOL_VERSION);
return dispatcher;
}
@@ -737,7 +737,6 @@ 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 = () => {
const pending = tools.get(callId);
@@ -748,7 +747,13 @@ async function waitForTool(call: RunnerToolCall): Promise<unknown> {
call.signal.addEventListener("abort", abort, { once: true });
tools.set(callId, {
turnId: activeTurnId,
settle,
settle: (result) => {
// A pipe write alone does not prove receiver admission. Only this
// call's turn-bound tool.resolve success confirms runnerd accepted
// the validated body; rejection, cancellation and timeout stay null.
commitNormalizedInput?.();
settle(result);
},
reject,
cleanup: () => call.signal.removeEventListener("abort", abort),
});
@@ -296,7 +296,7 @@ describe("NativeExecutionInputV1", () => {
})).toThrow("eventExpiryDays");
});
it.each([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] as const)("accepts only a closed ACPX profile matching the driver and agent at profile version %s", (agentProfileVersion) => {
it.each([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] as const)("accepts only a closed ACPX profile matching the driver and agent at profile version %s", (agentProfileVersion) => {
const provider = {
kind: "acpx",
agent: "pi",
@@ -328,7 +328,7 @@ describe("NativeExecutionInputV1", () => {
profile: provider.profile,
});
expect(parseNativeExecutionInput(parsed)).toEqual(parsed);
for (const unsupportedVersion of [0, 12, 1.5, "11", null]) {
for (const unsupportedVersion of [0, 13, 1.5, "12", null]) {
expect(() => parseNativeExecutionInput({
...input,
session: { ...input.session, driverKind: "acpx_runtime" },
@@ -91,7 +91,7 @@ export interface NativeAcpxProfileSnapshot {
protocolVersion: 1;
acpxVersion: "0.13.1";
agent: NativeAcpxAgent;
agentProfileVersion: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11;
agentProfileVersion: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
agentServerPackage: string;
agentServerVersion: string;
agentRuntimePackage: string | null;
@@ -608,7 +608,7 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput
|| profile.protocolVersion !== 1
|| profile.acpxVersion !== "0.13.1"
|| profile.agent !== provider.agent
|| (profile.agentProfileVersion !== 1 && profile.agentProfileVersion !== 2 && profile.agentProfileVersion !== 3 && profile.agentProfileVersion !== 4 && profile.agentProfileVersion !== 5 && profile.agentProfileVersion !== 6 && profile.agentProfileVersion !== 7 && profile.agentProfileVersion !== 8 && profile.agentProfileVersion !== 9 && profile.agentProfileVersion !== 10 && profile.agentProfileVersion !== 11)
|| (profile.agentProfileVersion !== 1 && profile.agentProfileVersion !== 2 && profile.agentProfileVersion !== 3 && profile.agentProfileVersion !== 4 && profile.agentProfileVersion !== 5 && profile.agentProfileVersion !== 6 && profile.agentProfileVersion !== 7 && profile.agentProfileVersion !== 8 && profile.agentProfileVersion !== 9 && profile.agentProfileVersion !== 10 && profile.agentProfileVersion !== 11 && profile.agentProfileVersion !== 12)
) {
throw new NativeExecutionInputError("input.provider.profile does not match the qualified ACPX v1 profile");
}
@@ -59,13 +59,14 @@ async function permissionPolicyClosure(extraClassifierImport = false): Promise<S
vi.mock("./installation-integrity.js", () => ({ verifyNativeAcpxInstallation: vi.fn() }));
describe("Copilot build-owned installation", () => {
it("admits the current v11 declaration and binds its source policy, receipts and patch hashes", () => {
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v11-identity.json", import.meta.url), "utf8"));
it("admits the current v12 declaration and binds its source policy, receipts and patch hashes", () => {
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v12-identity.json", import.meta.url), "utf8"));
expect(identity.declaration.systemInstructionDelivery).toBe(COPILOT_SYSTEM_INSTRUCTION_DELIVERY);
expect(identity.declaration.sharedRuntimeContract).toBe("paperclip.acpx-runtime-contract.v1");
expect(identity.declaration.permissionContextContract).toBe(COPILOT_PERMISSION_CONTEXT_CONTRACT);
expect(identity.declaration.semanticToolReceiptContract).toBe("paperclip.semantic_tool_receipt.v2");
expect(identity.declaration.semanticNormalizedInputContract).toBe("validated-forwarded-input-commit-v1");
expect(identity.declaration.semanticSidecarReceiptCommitContract).toBe("correlated-tool-resolve-v1");
expect(identity.declaration.messageIdentityContract).toBe("copilot-native-message-id-v1");
expect(identity.declaration.ownedDistributionDelivery).toBe("COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1");
const inventory = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-message-identity-distributions-1.0.88.json", import.meta.url), "utf8"));
@@ -90,7 +91,7 @@ describe("Copilot build-owned installation", () => {
expect([...closure].filter(path => !bound.has(path))).toEqual(["negative-control:unbound-policy"]);
});
it.each([...PERMISSION_POLICY_SOURCES, ...SEMANTIC_RECEIPT_SOURCES])("changing %s changes the candidate identity", (_path, field) => {
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v11-identity.json", import.meta.url), "utf8"));
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v12-identity.json", import.meta.url), "utf8"));
identity.declaration[field] = "0".repeat(64);
const sorted = Object.fromEntries(Object.entries(identity.declaration).sort(([a], [b]) => a.localeCompare(b)));
expect(`sha256:${createHash("sha256").update(JSON.stringify(sorted)).digest("hex")}`).not.toBe(identity.commandDigest);
@@ -72,10 +72,10 @@ export const QUALIFIED_ACPX_PROFILES: Readonly<
},
copilot: {
driverKind: ACPX_DRIVER_KIND, protocolVersion: ACPX_DRIVER_PROTOCOL_VERSION,
acpxVersion: QUALIFIED_ACPX_VERSION, agent: "copilot", agentProfileVersion: 11,
acpxVersion: QUALIFIED_ACPX_VERSION, agent: "copilot", agentProfileVersion: 12,
agentServerPackage: "@github/copilot", agentServerVersion: "1.0.88",
agentRuntimePackage: null, agentRuntimeVersion: null,
commandDigest: "sha256:d56589c43437277b527ed61155de7d882e3ae5316d67502d8b98a0d435f69f99",
commandDigest: "sha256:48cecd8dc77a5533240fcf2f29d19be05380da4a79f8e5061480f94241db75a8",
// 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",
@@ -34,6 +34,7 @@ describe("ACPX recovery identity", () => {
["copilot", "../../../test/fixtures/copilot-profile-v8-identity.json"],
["copilot", "../../../test/fixtures/copilot-profile-v9-identity.json"],
["copilot", "../../../test/fixtures/copilot-profile-v10-identity.json"],
["copilot", "../../../test/fixtures/copilot-profile-v11-identity.json"],
["pi", "../../../test-fixtures/pi-acp/profile-v9-identity.json"],
] as const)("rejects retained %s sessions after the execution identity changes", async (agent, path) => {
const fixture = await recoveryFixture();
@@ -0,0 +1,43 @@
{
"commandDigest": "sha256:48cecd8dc77a5533240fcf2f29d19be05380da4a79f8e5061480f94241db75a8",
"declaration": {
"schema": "paperclip.acpx_profile_declaration.v1",
"agent": "copilot",
"agentProfileVersion": 12,
"acpxVersion": "0.13.1",
"agentServerVersion": "1.0.88",
"protocolPolicyRevision": "copilot-agent-manual-v1",
"acpxPatchSha256": "bd5393058a218040d217fa85449d59a6f30507de54cd645bf0ef21422823f85e",
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
"agentFilesBinding": "registered-runtime-context-v1",
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
"messageIdentityContract": "copilot-native-message-id-v1",
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
"permissionContextContract": "copilot-edit-permission-context-v1",
"permissionContextSourceSha256": "fe1bb611bea289d81b1c7c973fbb01b93de133a7caa8fe78b7ff4ee1f485b224",
"permissionAdapterSourceSha256": "49421a779c76e286ce65d75052026eab9630b1995af9ccf045a2d43d284633ce",
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
"permissionRedactionSourceSha256": "687270d7bbe3dccce6c2fed8664e76cda724b43e545cb153b9f54b018de52927",
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
"permissionIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
"semanticBridgeSourceSha256": "8a43ebafb7a85dd1714ed6945670472666dcb63e8af32b4a74644163fa0e370d",
"toolEvidenceSourceSha256": "4b517eed447615b3cc820af348ccc360978175149caa0bbc838d1f78332abe75",
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
"semanticSidecarSourceSha256": "d58076d59ec23fe2261cba3aa0dca3e9f189105f0a5fa7ca3eeaac05cb338f31",
"semanticDirectDriverSourceSha256": "31e36917ee3592fa6d8f4b86632129fdffae20c45aed2e0e367ccf523e064f88",
"semanticValidationSourceSha256": "039fca52266fe21ba74ca64660d21228314d8ed737b19ba67941bfb25086e4d5",
"semanticNormalizationSourceSha256": "b1ce59251bf9c671f66e0e5173646190b9a2d1bfc5bdb7fb123824213986b92b",
"semanticCompletionContractSourceSha256": "00a684f9aea87f68eabf0974681c52461bc308631a1252a89c4ed087d7158b28",
"semanticValidatorsSourceSha256": "950b49b233d8dd62ff858398becfc55f0a181160cb8a434d9221b5c17816867c",
"semanticSchemaBundleSourceSha256": "bb55e18c5563bf5ec226e301c3399229304f295e986865e96554f193535368f2",
"semanticSidecarProtocolSourceSha256": "165cad76f224d60f0dc7928736d47a16787eaa8ab9e4c2222488da5fc8ad632e",
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1"
}
}