diff --git a/doc/architecture/runner-rich-acp-capabilities.md b/doc/architecture/runner-rich-acp-capabilities.md index 7e550024446..63583450485 100644 --- a/doc/architecture/runner-rich-acp-capabilities.md +++ b/doc/architecture/runner-rich-acp-capabilities.md @@ -1,6 +1,6 @@ # Rich ACP integration and qualification report -Updated: 2026-09-29. Foundation PR #14430 is merged at +Updated: 2026-09-30 UTC. Foundation PR #14430 is merged at `24beb005755465f71a19ec92a85da0958d1b9740`. Basic extended Runner Eval definitions are merged in `paperclip-evals` PR #29. Provider PRs remain separate review units and all three providers remain **pending qualification**. @@ -9,7 +9,17 @@ The [September 30 UTC evidence checkpoint](runner-rich-acp-validation-2026-09-30 separates current paid behavior, retained failures, runtime identities, billing coverage and incomplete qualification from historical results below. -Current candidates are **Cursor v5, Copilot v5 and Pi v8**. On corrected source +Current candidates are **Cursor v6, Copilot v6 and Pi v9**, pending fresh builds +and qualification. Their identities bind shared runtime contract +`paperclip.acpx-runtime-contract.v1`. The server includes that revision in the +warm-session configuration fingerprint for every ACPX agent, so an existing +owner cannot bypass changed cancellation and permission handling. Other +transports retain their previous configuration digest. Current profile identity +and admission tests pass (58 tests; one optional installation test skipped), and +four server tests verify changed ACPX owners retire while unchanged Codex +app-server owners remain reusable. These checks are not paid qualification. + +The most recent paid profiles are **Cursor v5, Copilot v5 and Pi v8**. On corrected source `5605c350b`, Cursor has passed all five basic paid local journeys: completion, semantic questions, semantic plan approval, controller restart and file editing/validation. Copilot has passed the paid Daytona completion journey; @@ -47,7 +57,7 @@ recommends Cursor and Copilot, followed by Pi, using the existing qualified ACPX path. Codex app-server is the richness benchmark. The legacy Cursor and Pi adapters are outside this change. -## Current profile evidence (2026-09-29, collected through September 30 UTC) +## Retained paid profile evidence (2026-09-29, collected through September 30 UTC) The latest paid runtime source is `5605c350b2a4dae75be3779f164d7e1a0e4a5a87`. It includes the passive native-read evidence fix from `794e90a258`: correlation @@ -141,10 +151,14 @@ timeout changed the run to failed. Its watcher also reported incomplete coverage while monitoring a workspace parent that startup can modify. The 144.581-second attempt retired every owned process, left no owned semaphores, and passed the full end audit; additional billing remained disabled at $0. -Shared cancellation settlement and explicit permission-provider provenance need -runtime repairs. The denial fixture needs an isolated target parent and coverage -diagnostics while retaining its strict completeness check. Paid runs are held -until the corrected runtime is identified and verified. +Shared cancellation settlement and explicit permission-provider provenance are +being repaired. Fixture commit `98a29fcf7` creates an isolated denied-target +parent before dispatch for local Copilot, Cursor and Pi. Its watcher retains +coverage failure reasons, parent identities and a bounded event journal; exact +native path correlation, complete observation and zero target mutation remain +required. All 80 focused fixture tests and fixture typecheck pass. Remote sealed +observers are unchanged. Paid runs are held until the corrected runtime is +identified and verified. The Copilot cloud cell also retired all 46 observed local processes, restored the host semaphore count from 321 to its baseline 310, removed its owned @@ -164,7 +178,7 @@ mode values; all 56 recovery/lifecycle tests and the Runner TypeScript build pass. The corrected-build Cursor cases above now pass the same strict validator. The original failed receipt remains a failure. -The current macOS ARM64 pack is +The most recent paid macOS ARM64 pack is `sha256:3940fbe1c7b197b964b8f8182c1f1ae548cff822a95ecb4539e0ca851b4f2032`. The rebuilt Linux x64 pack is `sha256:83702fae11105f5b9ee8f9731b86dc06becb12075f68bdc98dcdfe9f9c842473`, diff --git a/packages/paperclip-runner/scripts/build-copilot-distribution.mjs b/packages/paperclip-runner/scripts/build-copilot-distribution.mjs index 672523a04fa..9e70435283b 100644 --- a/packages/paperclip-runner/scripts/build-copilot-distribution.mjs +++ b/packages/paperclip-runner/scripts/build-copilot-distribution.mjs @@ -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:ece77e40876631a69a828b91813722001d71b6fc81be47ecd4b3dced84ff9473"; +const PROFILE_DIGEST = "sha256:0fe49c2f8a2b144344d0de745fed1e4568df305de3a26c3a121dec21c8d4b751"; /** 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 } = {}) { diff --git a/packages/paperclip-runner/scripts/candidate-provider-pack.mjs b/packages/paperclip-runner/scripts/candidate-provider-pack.mjs index 5a9caf0078c..3376197a525 100644 --- a/packages/paperclip-runner/scripts/candidate-provider-pack.mjs +++ b/packages/paperclip-runner/scripts/candidate-provider-pack.mjs @@ -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:aa8c0b2b84786982bcd06b7634bf95be6f2bc42bb8def48a8751dc50cf739b6b", + profileDigest: "sha256:377dcea64a727ce799cc112458d4b40ba4bc6574cd6c6f7233b6efd5917a6c4b", closureDigest: `sha256:${result.closureSha256}` }; } throw new Error(`The ${provider} candidate distribution builder is not included in this source revision`); diff --git a/packages/paperclip-runner/src/contracts/native-execution.test.ts b/packages/paperclip-runner/src/contracts/native-execution.test.ts index 557de37d7c6..29b48c9c973 100644 --- a/packages/paperclip-runner/src/contracts/native-execution.test.ts +++ b/packages/paperclip-runner/src/contracts/native-execution.test.ts @@ -296,7 +296,7 @@ describe("NativeExecutionInputV1", () => { })).toThrow("eventExpiryDays"); }); - it.each([1, 2, 3, 4, 5, 6, 7, 8] 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] 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, 9, 1.5, "8", null]) { + for (const unsupportedVersion of [0, 10, 1.5, "9", null]) { expect(() => parseNativeExecutionInput({ ...input, session: { ...input.session, driverKind: "acpx_runtime" }, diff --git a/packages/paperclip-runner/src/contracts/native-execution.ts b/packages/paperclip-runner/src/contracts/native-execution.ts index b4414a8ea25..998166c6027 100644 --- a/packages/paperclip-runner/src/contracts/native-execution.ts +++ b/packages/paperclip-runner/src/contracts/native-execution.ts @@ -91,7 +91,7 @@ export interface NativeAcpxProfileSnapshot { protocolVersion: 1; acpxVersion: "0.13.1"; agent: NativeAcpxAgent; - agentProfileVersion: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8; + agentProfileVersion: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; 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 !== 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) ) { throw new NativeExecutionInputError("input.provider.profile does not match the qualified ACPX v1 profile"); } diff --git a/packages/paperclip-runner/src/drivers/acpx/copilot-installation.test.ts b/packages/paperclip-runner/src/drivers/acpx/copilot-installation.test.ts index 2d474957057..a845bc62f5b 100644 --- a/packages/paperclip-runner/src/drivers/acpx/copilot-installation.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/copilot-installation.test.ts @@ -10,9 +10,10 @@ import { QUALIFIED_ACPX_PROFILES } from "./qualified-profiles.js"; vi.mock("./installation-integrity.js", () => ({ verifyNativeAcpxInstallation: vi.fn() })); describe("Copilot build-owned installation", () => { - it("admits the current v5 declaration and binds its source policy/patch hashes", () => { - const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v5-identity.json", import.meta.url), "utf8")); + it("admits the current v6 declaration and binds its source policy/patch hashes", () => { + const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v6-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(QUALIFIED_ACPX_PROFILES.copilot.agentProfileVersion).toBe(identity.declaration.agentProfileVersion); expect(QUALIFIED_ACPX_PROFILES.copilot.commandDigest).toBe(identity.commandDigest); const sorted = Object.fromEntries(Object.entries(identity.declaration).sort(([a], [b]) => a.localeCompare(b))); @@ -50,7 +51,7 @@ describe("Copilot build-owned installation", () => { }); it("rejects changed profile identities before native file access", async () => { vi.mocked(verifyNativeAcpxInstallation).mockClear(); - for (const override of [{ agentServerVersion: "latest" }, { commandDigest: "sha256:untrusted" }, { agent: "cursor" }, { agentProfileVersion: 1 }, { agentProfileVersion: 2 }, { agentProfileVersion: 3 }, { agentProfileVersion: 4 }, { agentProfileVersion: 6 }]) { + for (const override of [{ agentServerVersion: "latest" }, { commandDigest: "sha256:untrusted" }, { agent: "cursor" }, { agentProfileVersion: 1 }, { agentProfileVersion: 2 }, { agentProfileVersion: 3 }, { agentProfileVersion: 4 }, { agentProfileVersion: 5 }, { agentProfileVersion: 7 }]) { await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot, ...override } as never)).rejects.toThrow("exact pinned"); } expect(verifyNativeAcpxInstallation).not.toHaveBeenCalled(); diff --git a/packages/paperclip-runner/src/drivers/acpx/pi-installation.test.ts b/packages/paperclip-runner/src/drivers/acpx/pi-installation.test.ts index 6a29bfc552e..758a2e143bf 100644 --- a/packages/paperclip-runner/src/drivers/acpx/pi-installation.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/pi-installation.test.ts @@ -13,7 +13,7 @@ import { QUALIFIED_ACPX_PROFILES, type QualifiedAcpxProfile } from "./qualified- const roots: string[] = []; afterEach(async () => { vi.unstubAllEnvs(); await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true }))); }); -const candidate = (): QualifiedAcpxProfile => ({ ...QUALIFIED_ACPX_PROFILES.pi, agentProfileVersion: 8 }); +const candidate = (): QualifiedAcpxProfile => ({ ...QUALIFIED_ACPX_PROFILES.pi, agentProfileVersion: 9 }); async function fixture() { const root = await mkdtemp(join(tmpdir(), "pi-installation-test-")); roots.push(root); @@ -30,8 +30,9 @@ describe("Pi installation factory", () => { const hash = (bytes: string | Uint8Array) => createHash("sha256").update(bytes).digest("hex"); const canonical = (value: any): string => Array.isArray(value) ? `[${value.map(canonical).join(",")}]` : value && typeof value === "object" ? `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}` : JSON.stringify(value); - const identity = JSON.parse(await readFile(new URL("../../../test-fixtures/pi-acp/profile-v8-identity.json", import.meta.url), "utf8")); + const identity = JSON.parse(await readFile(new URL("../../../test-fixtures/pi-acp/profile-v9-identity.json", import.meta.url), "utf8")); const declaration = identity.declaration; + expect(declaration.sharedRuntimeContract).toBe("paperclip.acpx-runtime-contract.v1"); expect(identity.commandDigest).toBe(`sha256:${hash(canonical(declaration))}`); expect(QUALIFIED_ACPX_PROFILES.pi.commandDigest).toBe(identity.commandDigest); for (const key of ["agent", "agentProfileVersion", "acpxVersion", "agentServerVersion", "agentRuntimeVersion"] as const) { @@ -56,13 +57,14 @@ describe("Pi installation factory", () => { it("rejects legacy profiles and caller-selected identities", () => { expect(() => assertPiInstallationProfile(candidate())).not.toThrow(); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 1 })).toThrow("version 8"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 2 })).toThrow("version 8"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 3 })).toThrow("version 8"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 4 })).toThrow("version 8"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 5 })).toThrow("version 8"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 6 })).toThrow("version 8"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 7 })).toThrow("version 8"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 1 })).toThrow("version 9"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 2 })).toThrow("version 9"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 3 })).toThrow("version 9"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 4 })).toThrow("version 9"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 5 })).toThrow("version 9"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 6 })).toThrow("version 9"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 7 })).toThrow("version 9"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 8 })).toThrow("version 9"); for (const changed of [{ agentServerVersion: "latest" }, { commandDigest: `sha256:${"0".repeat(64)}` }, { reportedModelId: "different" }, { agentRuntimePackage: "ambient-pi" }]) { expect(() => assertPiInstallationProfile({ ...candidate(), ...changed })).toThrow("trusted declaration"); } diff --git a/packages/paperclip-runner/src/drivers/acpx/pi-installation.ts b/packages/paperclip-runner/src/drivers/acpx/pi-installation.ts index 24167a3afab..483efabb948 100644 --- a/packages/paperclip-runner/src/drivers/acpx/pi-installation.ts +++ b/packages/paperclip-runner/src/drivers/acpx/pi-installation.ts @@ -24,7 +24,7 @@ function record(value: unknown): Record { /** Candidate identity is build-owned; model selection cannot substitute a CLI. */ export function assertPiInstallationProfile(profile: QualifiedAcpxProfile): void { const trusted = QUALIFIED_ACPX_PROFILES.pi; - if (profile.agentProfileVersion !== 8) throw new Error("Pi rich ACP requires profile version 8; reopen the previous session"); + if (profile.agentProfileVersion !== 9) throw new Error("Pi rich ACP requires profile version 9; reopen the previous session"); if (profile.agent !== "pi" || profile.driverKind !== trusted.driverKind || profile.protocolVersion !== trusted.protocolVersion || profile.acpxVersion !== trusted.acpxVersion || profile.agentServerPackage !== "pi-acp" || profile.agentServerVersion !== "0.0.33" || profile.agentRuntimePackage !== "@earendil-works/pi-coding-agent" || profile.agentRuntimeVersion !== "0.84.2" || profile.commandDigest !== trusted.commandDigest || profile.permissionPolicy !== "interactive" || profile.qualificationModel !== PI_MODEL || profile.reportedModelId !== PI_MODEL) throw new Error("Pi distribution profile differs from its trusted declaration"); } diff --git a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.test.ts b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.test.ts index 8155eae9f2d..8f1bde78aeb 100644 --- a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.test.ts @@ -63,10 +63,11 @@ describe("qualified ACPX profiles", () => { }); }); -it("binds Cursor v5 to inspectable native instructions, closures and exact ACPX guard patch", () => { - const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/cursor-acp/profile-v5-identity.json", import.meta.url), "utf8")); +it("binds Cursor v6 to inspectable native instructions, closures and exact ACPX guard patch", () => { + const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/cursor-acp/profile-v6-identity.json", import.meta.url), "utf8")); const distribution = JSON.parse(readFileSync(new URL("../../../cursor-distributions.json", import.meta.url), "utf8")); expect(identity.declaration.distribution).toEqual(distribution); + expect(identity.declaration.sharedRuntimeContract).toBe("paperclip.acpx-runtime-contract.v1"); expect(identity.declaration.sessionModeAdmission).toBe("native-config-ack-recovery-bound-v1"); expect(identity.declaration.sessionModes).toEqual(["agent", "plan", "ask"]); expect(identity.declaration.defaultSessionMode).toBe("agent"); diff --git a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts index ec9afe73652..31a88b93fee 100644 --- a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts +++ b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts @@ -47,24 +47,24 @@ export const QUALIFIED_ACPX_PROFILES: Readonly< protocolVersion: ACPX_DRIVER_PROTOCOL_VERSION, acpxVersion: QUALIFIED_ACPX_VERSION, agent: "pi", - agentProfileVersion: 8, + agentProfileVersion: 9, qualificationStatus: "pending", agentServerPackage: "pi-acp", agentServerVersion: "0.0.33", agentRuntimePackage: "@earendil-works/pi-coding-agent", agentRuntimeVersion: "0.84.2", commandDigest: - "sha256:843d30e419914529755c9827d9151a306da1b50b643be7eab1abe797641a37ce", + "sha256:edf058835ee84de3869c4a8e8bdb71a934ffdb9f34daa37ae6faeb92371d1cdf", qualificationModel: "openrouter/deepseek/deepseek-v4-flash-0731", reportedModelId: "openrouter/deepseek/deepseek-v4-flash-0731", permissionPolicy: "interactive", }, cursor: { driverKind: ACPX_DRIVER_KIND, protocolVersion: ACPX_DRIVER_PROTOCOL_VERSION, - acpxVersion: QUALIFIED_ACPX_VERSION, agent: "cursor", agentProfileVersion: 5, + acpxVersion: QUALIFIED_ACPX_VERSION, agent: "cursor", agentProfileVersion: 6, agentServerPackage: "cursor-agent", agentServerVersion: "2026.09.26-dd393fe", agentRuntimePackage: null, agentRuntimeVersion: null, - commandDigest: "sha256:aa8c0b2b84786982bcd06b7634bf95be6f2bc42bb8def48a8751dc50cf739b6b", + commandDigest: "sha256:377dcea64a727ce799cc112458d4b40ba4bc6574cd6c6f7233b6efd5917a6c4b", // 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", @@ -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: 5, + acpxVersion: QUALIFIED_ACPX_VERSION, agent: "copilot", agentProfileVersion: 6, agentServerPackage: "@github/copilot", agentServerVersion: "1.0.88", agentRuntimePackage: null, agentRuntimeVersion: null, - commandDigest: "sha256:ece77e40876631a69a828b91813722001d71b6fc81be47ecd4b3dced84ff9473", + commandDigest: "sha256:0fe49c2f8a2b144344d0de745fed1e4568df305de3a26c3a121dec21c8d4b751", // 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", diff --git a/packages/paperclip-runner/test-fixtures/pi-acp/profile-v9-identity.json b/packages/paperclip-runner/test-fixtures/pi-acp/profile-v9-identity.json new file mode 100644 index 00000000000..bf1e7ce1576 --- /dev/null +++ b/packages/paperclip-runner/test-fixtures/pi-acp/profile-v9-identity.json @@ -0,0 +1,30 @@ +{ + "commandDigest": "sha256:edf058835ee84de3869c4a8e8bdb71a934ffdb9f34daa37ae6faeb92371d1cdf", + "declaration": { + "schema": "paperclip.acpx_profile_declaration.v1", + "agent": "pi", + "agentProfileVersion": 9, + "acpxVersion": "0.13.1", + "agentServerVersion": "0.0.33", + "agentRuntimeVersion": "0.84.2", + "nodeVersion": "24.21.0", + "closure": { + "darwin-arm64": "0187153354338cf4919d213dd2977e1be1073ad3941c7b9b5c8a09c5b32828e4", + "darwin-x64": "22c593576e78edb27bbe6234aabd31a7d65f6be7503683f568d0730ebe6c75a0", + "linux-x64": "016ce3653bdfc1c7269e3bf7d4bf26e48b0d0b7d409fd153f3f1b882e986654d" + }, + "wrapperSha256": "9fd9a685fb79b4f73a8dac08cbe360a6de87ffcb596cdaff1191e3ac57c24f56", + "helperSha256": "a19087c0af8fb3896fe3290280695f8f936574013bc4acde650f11f023f0f03b", + "extensionSha256": "2b74a4a2e6a42def3d9918a46732e0e1b8c618791d2b22970cfb3e9d285e55ad", + "agentFilesBinding": "registered-runtime-context-v1", + "nativeQuestions": "select-confirm-input-editor-v2", + "pathPolicy": "sdk-normalization-v1", + "mcpToolNames": "provider-safe-exact-source-v1", + "acpxPatchSha256": "79aad2d688b03362e8cfcbf7a08f78a8383869f6882a9c9ed66f1d18efb94f2b", + "nativeAssistantMessages": "sdk-boundaries-v1", + "nativeNotices": "session-extension-v1", + "messageProjectionSha256": "17d1e91b948dfa6a56e1905645add52b502041ad843c8d50a13b07956f22aadd", + "noticeProjectionSha256": "c619080ec72a577889c382d44ea9024b10efdb9d83e933c03bfdd8da79ede591", + "sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1" + } +} diff --git a/packages/paperclip-runner/test/fixtures/copilot-profile-v6-identity.json b/packages/paperclip-runner/test/fixtures/copilot-profile-v6-identity.json new file mode 100644 index 00000000000..840c089fa41 --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/copilot-profile-v6-identity.json @@ -0,0 +1,17 @@ +{ + "commandDigest": "sha256:0fe49c2f8a2b144344d0de745fed1e4568df305de3a26c3a121dec21c8d4b751", + "declaration": { + "schema": "paperclip.acpx_profile_declaration.v1", + "agent": "copilot", + "agentProfileVersion": 6, + "acpxVersion": "0.13.1", + "agentServerVersion": "1.0.88", + "protocolPolicyRevision": "copilot-agent-manual-v1", + "acpxPatchSha256": "79aad2d688b03362e8cfcbf7a08f78a8383869f6882a9c9ed66f1d18efb94f2b", + "policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3", + "distributionSourceSha256": "bb3dcf14972e82f6578c8250540cfbc6ec2ad953bea5d4f10d28c8b461011f3f", + "agentFilesBinding": "registered-runtime-context-v1", + "systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1", + "sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1" + } +} diff --git a/packages/paperclip-runner/test/fixtures/cursor-acp/profile-v6-identity.json b/packages/paperclip-runner/test/fixtures/cursor-acp/profile-v6-identity.json new file mode 100644 index 00000000000..55270a55d6a --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/cursor-acp/profile-v6-identity.json @@ -0,0 +1,54 @@ +{ + "commandDigest": "sha256:377dcea64a727ce799cc112458d4b40ba4bc6574cd6c6f7233b6efd5917a6c4b", + "declaration": { + "schema": "paperclip.acpx_profile_declaration.v1", + "agent": "cursor", + "agentProfileVersion": 6, + "acpxVersion": "0.13.1", + "agentServerVersion": "2026.09.26-dd393fe", + "patchVersion": "paperclip-cursor-instructions-v2", + "distribution": { + "schema": "paperclip.cursor_distribution.v1", + "version": "2026.09.26-dd393fe", + "platforms": { + "darwin-arm64": { + "url": "https://downloads.cursor.com/lab/2026.09.26-dd393fe/darwin/arm64/agent-cli-package.tar.gz", + "archiveSha256": "538827d96a779bab854a865c8e42859e8e87db34b5f69770261b90fc8cfff191", + "closureSha256": "912a37edb67fd7737809c24049dce2db20b1330f98b15a6e43809f0e0943cb6c", + "executable": "node", + "entrypoint": "index.js", + "vendorClosureSha256": "77394184a89b0e7384971181da19c3c82d83a399b04e7944b3f94fe3d7e62b25" + }, + "darwin-x64": { + "url": "https://downloads.cursor.com/lab/2026.09.26-dd393fe/darwin/x64/agent-cli-package.tar.gz", + "archiveSha256": "ed1771c44cbf0f8059c67cac0d939e6a60eb730bd066d7dc4cbb14de314cfb6e", + "closureSha256": "322f54c7bd533b202a311ce8e2c8916bfe0202790ef8a40fbb36bb89cbd506cb", + "executable": "node", + "entrypoint": "index.js", + "vendorClosureSha256": "7c8775160af74e5fb8d25a30e8412c1a6476b0661529078a686e5118fd1c5e5f" + }, + "linux-x64": { + "url": "https://downloads.cursor.com/lab/2026.09.26-dd393fe/linux/x64/agent-cli-package.tar.gz", + "archiveSha256": "8085fd120f5c71f4eae7fea26a043718e5644e3071e4fab3220a0e58c51f9593", + "closureSha256": "a375fc771dfe86b9b24532f95956ec4b02c5f4f24d80f5db06565160eb3b672c", + "executable": "node", + "entrypoint": "index.js", + "vendorClosureSha256": "bf04ef8ea6a63191067a6b3e15139aa2c793d8419daab246aa3f0a012e1bbcd9" + } + }, + "patchVersion": "paperclip-cursor-instructions-v2" + }, + "agentFilesBinding": "registered-runtime-context-v1", + "instructionBinding": "native-global-rules-v1", + "instructionAdmission": "synchronous-protocol-guard-v1", + "acpxPatchSha256": "79aad2d688b03362e8cfcbf7a08f78a8383869f6882a9c9ed66f1d18efb94f2b", + "sessionModeAdmission": "native-config-ack-recovery-bound-v1", + "sessionModes": [ + "agent", + "plan", + "ask" + ], + "defaultSessionMode": "agent", + "sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1" + } +} diff --git a/server/src/services/native-runtime/native-session-executor.test.ts b/server/src/services/native-runtime/native-session-executor.test.ts index 9cfa4544edc..b6916345f3d 100644 --- a/server/src/services/native-runtime/native-session-executor.test.ts +++ b/server/src/services/native-runtime/native-session-executor.test.ts @@ -54,6 +54,7 @@ import { } from "./native-harness-backup-stamp.js"; import { nativeRuntimeContextFixture } from "./runtime-context.test-fixture.js"; import { nativeToolContractFingerprintForTarget } from "./native-session-resume.js"; +import * as nativeSessionResume from "./native-session-resume.js"; import { buildNativeHeartbeatPreparationSpans } from "./native-run-trace.js"; import { NativeRunnerOwnershipUnverifiedError } from "./native-runner-ownership.js"; import type { AdapterRuntimeEvent } from "../../adapters/index.js"; @@ -7061,7 +7062,7 @@ describe("native warm session supervision", () => { }); }); - it.each(["permission", "managed credential"])("replaces an idle warm provider session when its %s changes", async (change) => { + it.each(["permission", "managed credential", "ACPX runtime contract", "unchanged Codex runtime contract"])("checks warm owner compatibility for %s", async (change) => { const firstClose = vi.fn(async () => undefined); const secondClose = vi.fn(async () => undefined); const firstSession = { close: firstClose }; @@ -7069,7 +7070,9 @@ describe("native warm session supervision", () => { const base = { ...execution, schema: "paperclip.native-execution-input.v4", - provider: { kind: "codex", model: null, approvalPolicy: "never" }, + provider: change === "ACPX runtime contract" + ? { kind: "acpx", agent: "claude", model: "claude-sonnet-5", permissionMode: "approve-all" } + : { kind: "codex", model: null, approvalPolicy: "never" }, binding: { ...execution.binding, runId: "run-permission-never", @@ -7083,7 +7086,7 @@ describe("native warm session supervision", () => { }, session: { normalizedSessionId: "session-warm-permission", - driverKind: "codex_app_server" as const, + driverKind: change === "ACPX runtime contract" ? "acpx_runtime" as const : "codex_app_server" as const, protocolVersion: 1 as const, lifecyclePolicy: { mode: "warm" as const, idleTimeoutMs: 20 }, }, @@ -7106,6 +7109,12 @@ describe("native warm session supervision", () => { highestContiguousSourceSeq: 1, usage: null, }; + const runtimeChange = change === "ACPX runtime contract" || change === "unchanged Codex runtime contract"; + const keepsOwner = change === "unchanged Codex runtime contract"; + const currentRuntimeContract = nativeSessionResume.nativeRuntimeContractForProvider(base.provider); + const contract = runtimeChange + ? vi.spyOn(nativeSessionResume, "nativeRuntimeContractForProvider").mockReturnValue(undefined) + : undefined; state.execute .mockReset() .mockImplementationOnce(async (options) => { @@ -7114,8 +7123,8 @@ describe("native warm session supervision", () => { return result; }) .mockImplementationOnce(async (options) => { - expect(options.existingSession).toBeUndefined(); - options.onSession?.(secondSession); + expect(options.existingSession).toBe(keepsOwner ? firstSession : undefined); + if (!keepsOwner) options.onSession?.(secondSession); return result; }); @@ -7129,21 +7138,22 @@ describe("native warm session supervision", () => { managedAiCredentialIdentity: "first-identity", runnerInstanceId: "runner", }); + contract?.mockReturnValue(currentRuntimeContract); await executePaperclipNativeSession({ db: leaseDb(lowered), execution: lowered, managedAiCredentialIdentity: change === "managed credential" ? "second-identity" : "first-identity", runnerInstanceId: "runner", }); - expect(firstClose).toHaveBeenCalledWith({ - reason: "warm native session configuration changed", - }); + if (keepsOwner) expect(firstClose).not.toHaveBeenCalled(); + else expect(firstClose).toHaveBeenCalledWith({ reason: "warm native session configuration changed" }); expect(secondClose).not.toHaveBeenCalled(); await vi.advanceTimersByTimeAsync(20); - expect(secondClose).toHaveBeenCalledWith({ + expect(keepsOwner ? firstClose : secondClose).toHaveBeenCalledWith({ reason: "warm native session idle timeout", }); } finally { + contract?.mockRestore(); vi.useRealTimers(); } }); diff --git a/server/src/services/native-runtime/native-session-executor.ts b/server/src/services/native-runtime/native-session-executor.ts index e07b82088da..41ecbb6d927 100644 --- a/server/src/services/native-runtime/native-session-executor.ts +++ b/server/src/services/native-runtime/native-session-executor.ts @@ -138,7 +138,7 @@ import { renderNativeRunnerStagedAttachmentPrompt, stageNativeRunnerWakeAttachments, } from "./native-runner-file-handoff.js"; -import { nativeToolContractFingerprintForTarget } from "./native-session-resume.js"; +import { nativeRuntimeContractForProvider, nativeToolContractFingerprintForTarget } from "./native-session-resume.js"; import { verifyRetainedMaintenanceNoLaunch } from "./native-maintenance-no-launch.js"; import { registerRunnerPrpAuthority } from "../../realtime/runner-prp-ws.js"; import { connectRunnerPrpIngress } from "../../realtime/runner-prp-outbound.js"; @@ -5210,6 +5210,7 @@ function nativeSessionConfigDigest( // without newly required tools. nativeToolContractFingerprint: nativeToolContractFingerprintForTarget(executionTargetKind), + runtimeContract: nativeRuntimeContractForProvider(execution.provider), }), ) .digest("hex")}`; diff --git a/server/src/services/native-runtime/native-session-resume.ts b/server/src/services/native-runtime/native-session-resume.ts index f50b6bb0962..91341e9e6c9 100644 --- a/server/src/services/native-runtime/native-session-resume.ts +++ b/server/src/services/native-runtime/native-session-resume.ts @@ -10,6 +10,16 @@ import { parseNativeExecutionInput } from "../../vendor/paperclip-runner/index.j export type NativeToolExecutionTargetKind = "local" | "remote"; +/** Fence live ACPX owners when shared runtime semantics change, even if the + * underlying native executable/profile did not change. Undefined preserves + * the existing configuration digest for other provider transports. + */ +export function nativeRuntimeContractForProvider( + provider: NativeExecutionInput["provider"], +): string | undefined { + return provider.kind === "acpx" ? "paperclip.acpx-runtime-contract.v1" : undefined; +} + /** * Persisted provider threads retain their dynamic-tool declarations. This * fingerprint is part of checkpoint compatibility and must change whenever diff --git a/tests/runner-e2e/README.md b/tests/runner-e2e/README.md index 869a44a38cb..c85f8aac051 100644 --- a/tests/runner-e2e/README.md +++ b/tests/runner-e2e/README.md @@ -1310,6 +1310,15 @@ See [Copilot native protection](./FIXTURES.md#copilot-native-protection) for the expected cancelled negative test, finite attached-process oracle, evidence limits, and required rebuilt runtime. Registration is not a qualification claim. +Local human-denial cases for Copilot, Cursor and Pi create a fresh fixture-owned +`pc-denied-*` directory before dispatch and watch its parent identity throughout +the attempt. This keeps unrelated workspace startup writes outside the target +watch. Exact prompt/native target correlation, complete watcher coverage and +zero target mutations remain required. Evidence includes coverage failure +reasons, parent device/inode changes and a bounded timestamped event journal; +an absent final file cannot hide an incomplete watch or transient mutation. +Remote cases retain their existing sealed observers. + ### Cursor native interactions (candidate)