diff --git a/doc/plans/2026-10-02-pi-production-readiness.md b/doc/plans/2026-10-02-pi-production-readiness.md index 4c0492c72a6..d53aefaaf24 100644 --- a/doc/plans/2026-10-02-pi-production-readiness.md +++ b/doc/plans/2026-10-02-pi-production-readiness.md @@ -9,7 +9,10 @@ qualification. The existing draft stack must be reviewed in dependency order. - Pi: `@earendil-works/pi-coding-agent@1.0.0`. - Wrapper: `pi-acp@0.0.33`; ACPX: `0.13.1`; Node: `24.21.0`. -- Pi profile: 13. Model: `openrouter/deepseek/deepseek-v4-flash-0731`. +- Pi profile: 14. Model: `openrouter/deepseek/deepseek-v4-flash-0731`. +- Profile 14 binds the corrected outbound ACPX client patch. Profile 13 remains + historical evidence. Cursor 11 and Copilot 15 also bind that shared patch; + both remain pending and receive no new paid qualification in this work. - Reasoning: native-confirmed `low`. No silent model or thinking fallback. - Exact source, suite definition, installed package integrity, runner digest, environment and cost evidence must accompany each attempt. @@ -22,7 +25,7 @@ qualification. The existing draft stack must be reviewed in dependency order. | Live lifecycle | Pending native question survives controller restart in the original process; one creation and resolution; stale answer after provider death rejected; Stop retires owned processes; three warm turns keep process identity | Installed local restart and three-turn warm continuity pass at `f5f57e380`. Pending-permission Stop and stale response rejection pass at `03dd6ef93`. Provider-death evidence remains required. | | Product workflows | All 26 explicit Pi Product E2E cells (13 local, 13 Daytona); screenshots, public state, independent artifacts, terminal and cleanup evidence | Historical partial passes exist. Full final-source campaign remains required. | | Runner protocol | Pi roster passes through the native packaged runner and authenticated mock control plane; lifecycle/denial/control cases remain distinct from Product tests | Installed get-task-context passes at `f5f57e380`; context-before-action times out. Five cells remain unexecuted. | -| Installed distribution | Public CLI/server tars on ARM Mac, Intel Mac and Linux; normal Pi setup; exact Linux companion and immutable Daytona image imported without binary override | Fresh ARM public installation and normal packaged no-key probe pass. The Linux fleet job is queued. Final-source Intel/Linux/Daytona receipts remain required. | +| Installed distribution | Public CLI/server tars on ARM Mac, Intel Mac and Linux; normal Pi setup; exact Linux companion and immutable Daytona image imported without binary override | Historical ARM and Intel public installations pass. Linux CI retains `session_handshake_timeout`; queued fleet jobs were cancelled. Final-source platform receipts, Linux companion and Daytona image remain required. | | Governance and spend | Company isolation, human-only permission, duplicate/stale answers, Stop and budget hard stop; pricing estimates never become claimed bills | 58 focused governance/cost/session invariants pass at `f5f57e380`. Live pending-permission Stop passes at `03dd6ef93`. The qualification key cap is not proof of Paperclip budget enforcement. | | Integration and rollout | Review each prerequisite; final-head typecheck, tests, build and CI pass; no unresolved review; exact artifacts; rollback recorded | Draft #14956 is stacked on #14924. Prerequisite PRs #14921–#14924 remain open. No merge or release. | @@ -42,6 +45,24 @@ prices are estimates. Unpriced usage must stay unpriced in the ledger and UI. ## Evidence so far +- At `36e712104`, installed steering returns HTTP 200 and the native command + journal records accepted delivery. The public API retains the correct + correlation-bound acknowledgement at source sequence 65 and also a + rehydrated transport echo at 66. The exact-one acknowledgement gate rejects + that duplicate before permission denial. A regression using the actual + rehydration function reproduces both items; suppressing the transport echo + retains the one authoritative item. The interrupted attempt remains failed. +- The shared ACPX patch also requires portable hunk metadata and new byte-bound + identities. Pi advances to 14, Cursor to pending 11, and Copilot to pending + 15. Historical fixtures remain immutable, and old installed identities fail + closed. The corrected patch passes all 16 packaging checks; identity and + steering regressions pass 244 tests. No other provider qualification expands. +- The credential-free Intel public installation at `36e712104` passes closed + admission in 44 seconds. Its Greptile review is 5/5, but CI is held by the + stale patch-bound profiles, portable patch metadata and Linux admission. + The paid attempt and task-owned Linux build were stopped when that identity + mismatch was found. Their evidence is retained; the owned server and database + processes are retired. Qualification must use newly installed profile 14. - At `4237bc369`, the native turn-binding correction advances the steering journey past `steering_stale_turn`, but the provider boundary still rejects delivery. The real patched ACPX client lacks `requestExtension`: its types diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs index ae1dfea4a64..559a4ed9539 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs @@ -209,7 +209,7 @@ impl AcpxProviderDescriptor { "0.0.33", Some("@earendil-works/pi-coding-agent"), Some("1.0.0"), - "sha256:fe1e6da01b2a9e4c691ca27cf689d2d6de846a93be6b23fc1e103c9addd7b177", + "sha256:f35145437eeb355ed37bc5a4fa93d7ede561d9c45daf311979b46890b808ddd4", ), "cursor" => ( self.model.as_str(), @@ -217,7 +217,7 @@ impl AcpxProviderDescriptor { "2026.09.26-dd393fe", None, None, - "sha256:1df2a15b93bc3a14fa47fa3315344ba023fe2412048047cdc6f32096a6336564", + "sha256:13207d7b6afcfe681d4fe9098655df056c2cf87e03f5141e961cd01c32c5bdd2", ), "copilot" => ( self.model.as_str(), @@ -225,7 +225,7 @@ impl AcpxProviderDescriptor { "1.0.88", None, None, - "sha256:5e5955c57917a7e7c25703b3ed9e420cd72105eab033b611508dc37fdba3858b", + "sha256:8faf47520f156c62c79ea6ca7d1d90a85ddc9a6621f798bd14e87bc6c643bd04", ), "grok" => ( "grok-4.7", @@ -3014,7 +3014,7 @@ mod tests { "cursor", "cursor-agent", "2026.09.26-dd393fe", - "sha256:1df2a15b93bc3a14fa47fa3315344ba023fe2412048047cdc6f32096a6336564", + "sha256:13207d7b6afcfe681d4fe9098655df056c2cf87e03f5141e961cd01c32c5bdd2", None, None, "explicit-model", @@ -3023,7 +3023,7 @@ mod tests { "copilot", "@github/copilot", "1.0.88", - "sha256:5e5955c57917a7e7c25703b3ed9e420cd72105eab033b611508dc37fdba3858b", + "sha256:8faf47520f156c62c79ea6ca7d1d90a85ddc9a6621f798bd14e87bc6c643bd04", None, None, "explicit-model", @@ -3032,7 +3032,7 @@ mod tests { "pi", "pi-acp", "0.0.33", - "sha256:fe1e6da01b2a9e4c691ca27cf689d2d6de846a93be6b23fc1e103c9addd7b177", + "sha256:f35145437eeb355ed37bc5a4fa93d7ede561d9c45daf311979b46890b808ddd4", Some("@earendil-works/pi-coding-agent"), Some("1.0.0"), "openrouter/deepseek/deepseek-v4-flash-0731", diff --git a/packages/paperclip-runner/runner/crates/runner-core/tests/native_provider_backend.rs b/packages/paperclip-runner/runner/crates/runner-core/tests/native_provider_backend.rs index 6e2dad3b946..5ffbfed2ba5 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/tests/native_provider_backend.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/tests/native_provider_backend.rs @@ -17,7 +17,7 @@ use sha2::{Digest, Sha256}; const CODEX_ACPX_DIGEST: &str = "sha256:c4538599d1ab767db5dff50934f13bb5ba313a59d9c4a83e993fac4617ea63d3"; const PI_ACPX_DIGEST: &str = - "sha256:fe1e6da01b2a9e4c691ca27cf689d2d6de846a93be6b23fc1e103c9addd7b177"; + "sha256:f35145437eeb355ed37bc5a4fa93d7ede561d9c45daf311979b46890b808ddd4"; fn temporary_directory(label: &str) -> PathBuf { let nonce = SystemTime::now() diff --git a/packages/paperclip-runner/scripts/build-copilot-distribution.mjs b/packages/paperclip-runner/scripts/build-copilot-distribution.mjs index 19a2418a1ab..c08f1f0bfca 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:5e5955c57917a7e7c25703b3ed9e420cd72105eab033b611508dc37fdba3858b"; +const PROFILE_DIGEST = "sha256:8faf47520f156c62c79ea6ca7d1d90a85ddc9a6621f798bd14e87bc6c643bd04"; /** 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 52dd5651afe..00854ccdde6 100644 --- a/packages/paperclip-runner/scripts/candidate-provider-pack.mjs +++ b/packages/paperclip-runner/scripts/candidate-provider-pack.mjs @@ -39,7 +39,7 @@ export async function materializeCandidateProviderPack({ provider, outputRoot }) if (provider === "cursor") { const result = await materializePinnedCursorDistribution({ destination: outputRoot }); return { version: result.version, - profileDigest: "sha256:1df2a15b93bc3a14fa47fa3315344ba023fe2412048047cdc6f32096a6336564", + profileDigest: "sha256:13207d7b6afcfe681d4fe9098655df056c2cf87e03f5141e961cd01c32c5bdd2", 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/backends/native-backend-factory.test.ts b/packages/paperclip-runner/src/backends/native-backend-factory.test.ts index 7daa9e54ae0..f7e4d38b1b3 100644 --- a/packages/paperclip-runner/src/backends/native-backend-factory.test.ts +++ b/packages/paperclip-runner/src/backends/native-backend-factory.test.ts @@ -568,6 +568,9 @@ describe("native backend factory", () => { it.each([ ["cursor", "../../test/fixtures/cursor-acp/profile-v7-identity.json"], + ["cursor", "../../test/fixtures/cursor-acp/profile-v10-identity.json"], + ["copilot", "../../test/fixtures/copilot-profile-v14-identity.json"], + ["pi", "../../test-fixtures/pi-acp/profile-v13-identity.json"], ["cursor", "../../test/fixtures/cursor-acp/profile-v9-identity.json"], ["copilot", "../../test/fixtures/copilot-profile-v7-identity.json"], ["copilot", "../../test/fixtures/copilot-profile-v9-identity.json"], diff --git a/packages/paperclip-runner/src/contracts/native-execution.test.ts b/packages/paperclip-runner/src/contracts/native-execution.test.ts index e2c4edcd45b..491f40a8d4e 100644 --- a/packages/paperclip-runner/src/contracts/native-execution.test.ts +++ b/packages/paperclip-runner/src/contracts/native-execution.test.ts @@ -344,7 +344,7 @@ describe("NativeExecutionInputV1", () => { profile: provider.profile, }); expect(parseNativeExecutionInput(parsed)).toEqual(parsed); - for (const unsupportedVersion of [0, 15, 1.5, "14", null]) { + for (const unsupportedVersion of [0, 16, 1.5, "14", null]) { expect(() => parseNativeExecutionInput({ ...input, session: { ...input.session, driverKind: "acpx_runtime" }, @@ -521,7 +521,7 @@ describe("native task context ownership", () => { }); } - it.each(["off", "low", "high", "max"])("requires exact Pi13 thinking level %s in native input", piThinkingLevel => { + it.each(["off", "low", "high", "max"])("requires exact current Pi thinking level %s in native input", piThinkingLevel => { const { qualificationModel, reportedModelId: _reported, permissionPolicy: _permission, modelPolicy: _policy, qualificationStatus: _status, ...profile } = QUALIFIED_ACPX_PROFILES.pi; const value = { ...currentInput(), session: { ...currentInput().session, driverKind: "acpx_runtime" }, provider: { kind: "acpx", agent: "pi", model: qualificationModel, permissionMode: "approve-all", piThinkingLevel, profile } }; expect(parseNativeExecutionInput(value).provider).toEqual(value.provider); diff --git a/packages/paperclip-runner/src/contracts/native-execution.ts b/packages/paperclip-runner/src/contracts/native-execution.ts index bc381f32e3c..990026a6f71 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 | 9 | 10 | 11 | 12 | 13 | 14; + agentProfileVersion: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15; agentServerPackage: string; agentServerVersion: string; agentRuntimePackage: string | null; @@ -578,9 +578,6 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput if (provider.piThinkingLevel !== undefined && (provider.agent !== "pi" || (typeof provider.piThinkingLevel !== "string" || !["off", "low", "high", "max"].includes(provider.piThinkingLevel)))) { throw new NativeExecutionInputError("input.provider.piThinkingLevel must be off, low, high, or max and is supported only for Pi"); } - if (provider.agent === "pi" && record(provider.profile, "input.provider.profile").agentProfileVersion === 13 && provider.piThinkingLevel === undefined) { - throw new NativeExecutionInputError("input.provider.piThinkingLevel is required for Pi profile 13"); - } if (provider.cursorMode !== undefined && (provider.agent !== "cursor" || (provider.cursorMode !== "agent" && provider.cursorMode !== "plan" && provider.cursorMode !== "ask"))) { throw new NativeExecutionInputError("input.provider.cursorMode must be agent, plan, or ask and is supported only for Cursor"); @@ -616,10 +613,13 @@ 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 !== 12 && profile.agentProfileVersion !== 13 && profile.agentProfileVersion !== 14) + || (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 && profile.agentProfileVersion !== 13 && profile.agentProfileVersion !== 14 && profile.agentProfileVersion !== 15) ) { throw new NativeExecutionInputError("input.provider.profile does not match the qualified ACPX v1 profile"); } + if (provider.agent === "pi" && profile.agentProfileVersion >= 13 && provider.piThinkingLevel === undefined) { + throw new NativeExecutionInputError("input.provider.piThinkingLevel is required for Pi profile 13 or later"); + } const runtimePackage = nullableText(profile.agentRuntimePackage, "input.provider.profile.agentRuntimePackage"); const runtimeVersion = nullableText(profile.agentRuntimeVersion, "input.provider.profile.agentRuntimeVersion"); if ((runtimePackage === null) !== (runtimeVersion === null)) { 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 4637385a1c6..469ff040246 100644 --- a/packages/paperclip-runner/src/drivers/acpx/copilot-installation.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/copilot-installation.test.ts @@ -80,8 +80,15 @@ describe("Copilot build-owned installation", () => { expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBe("pending"); }); - it("admits the current v14 declaration and binds its source policy, receipts and patch hashes", () => { - const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v14-identity.json", import.meta.url), "utf8")); + it("preserves v14 history while binding the corrected outbound ACPX delivery", () => { + const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v14-identity.json", import.meta.url), "utf8")); + const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v15-identity.json", import.meta.url), "utf8")); + expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual(["acpxPatchSha256", "agentProfileVersion"]); + expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBe("pending"); + }); + + it("admits the current v15 declaration and binds its source policy, receipts and patch hashes", () => { + const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v15-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); @@ -112,7 +119,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-v14-identity.json", import.meta.url), "utf8")); + const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v15-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); 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 82bf7fe2a28..e2a6385b75d 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: 13 }); +const candidate = (): QualifiedAcpxProfile => ({ ...QUALIFIED_ACPX_PROFILES.pi, agentProfileVersion: 14 }); async function fixture() { const root = await mkdtemp(join(tmpdir(), "pi-installation-test-")); roots.push(root); @@ -26,11 +26,17 @@ async function fixture() { } describe("Pi installation factory", () => { + it("preserves profile 13 and changes only the version and corrected ACPX patch binding", async () => { + const prior = JSON.parse(await readFile(new URL("../../../test-fixtures/pi-acp/profile-v13-identity.json", import.meta.url), "utf8")); + const current = JSON.parse(await readFile(new URL("../../../test-fixtures/pi-acp/profile-v14-identity.json", import.meta.url), "utf8")); + expect(Object.keys(current.declaration).filter(key => JSON.stringify(current.declaration[key]) !== JSON.stringify(prior.declaration[key])).sort()).toEqual(["acpxPatchSha256", "agentProfileVersion"]); + }); + it("binds the profile declaration to the reviewed patch and platform closure pins", async () => { 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-v13-identity.json", import.meta.url), "utf8")); + const identity = JSON.parse(await readFile(new URL("../../../test-fixtures/pi-acp/profile-v14-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))}`); @@ -57,18 +63,19 @@ describe("Pi installation factory", () => { it("rejects legacy profiles and caller-selected identities", () => { expect(() => assertPiInstallationProfile(candidate())).not.toThrow(); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 1 })).toThrow("version 13"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 2 })).toThrow("version 13"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 3 })).toThrow("version 13"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 4 })).toThrow("version 13"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 5 })).toThrow("version 13"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 6 })).toThrow("version 13"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 7 })).toThrow("version 13"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 8 })).toThrow("version 13"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 9 })).toThrow("version 13"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 10 })).toThrow("version 13"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 11 })).toThrow("version 13"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 12 })).toThrow("version 13"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 1 })).toThrow("version 14"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 2 })).toThrow("version 14"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 3 })).toThrow("version 14"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 4 })).toThrow("version 14"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 5 })).toThrow("version 14"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 6 })).toThrow("version 14"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 7 })).toThrow("version 14"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 8 })).toThrow("version 14"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 9 })).toThrow("version 14"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 10 })).toThrow("version 14"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 11 })).toThrow("version 14"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 12 })).toThrow("version 14"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 13 })).toThrow("version 14"); 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 6f304ed5422..3bdb1f8f30d 100644 --- a/packages/paperclip-runner/src/drivers/acpx/pi-installation.ts +++ b/packages/paperclip-runner/src/drivers/acpx/pi-installation.ts @@ -25,7 +25,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 !== 13) throw new Error("Pi rich ACP requires profile version 13; reopen the previous session"); + if (profile.agentProfileVersion !== 14) throw new Error("Pi rich ACP requires profile version 14; 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 !== "1.0.0" || 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 28bd77a7b32..461392a0f88 100644 --- a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.test.ts @@ -63,8 +63,11 @@ describe("qualified ACPX profiles", () => { }); }); -it("binds Cursor v10 to native instructions, tool identity, closures and the exact ACPX guard patch", () => { - const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/cursor-acp/profile-v10-identity.json", import.meta.url), "utf8")); +it("binds Cursor v11 to native instructions, tool identity, closures and the exact ACPX guard patch", () => { + const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/cursor-acp/profile-v10-identity.json", import.meta.url), "utf8")); + const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/cursor-acp/profile-v11-identity.json", import.meta.url), "utf8")); + expect(Object.keys(identity.declaration).filter(key => JSON.stringify(identity.declaration[key]) !== JSON.stringify(prior.declaration[key])).sort()).toEqual(["acpxPatchSha256", "agentProfileVersion"]); + expect(QUALIFIED_ACPX_PROFILES.cursor.qualificationStatus).toBe("pending"); 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"); diff --git a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts index a4aa425d9ee..e7881e98e38 100644 --- a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts +++ b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts @@ -47,23 +47,23 @@ export const QUALIFIED_ACPX_PROFILES: Readonly< protocolVersion: ACPX_DRIVER_PROTOCOL_VERSION, acpxVersion: QUALIFIED_ACPX_VERSION, agent: "pi", - agentProfileVersion: 13, + agentProfileVersion: 14, agentServerPackage: "pi-acp", agentServerVersion: "0.0.33", agentRuntimePackage: "@earendil-works/pi-coding-agent", agentRuntimeVersion: "1.0.0", commandDigest: - "sha256:fe1e6da01b2a9e4c691ca27cf689d2d6de846a93be6b23fc1e103c9addd7b177", + "sha256:f35145437eeb355ed37bc5a4fa93d7ede561d9c45daf311979b46890b808ddd4", 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: 10, + acpxVersion: QUALIFIED_ACPX_VERSION, agent: "cursor", agentProfileVersion: 11, agentServerPackage: "cursor-agent", agentServerVersion: "2026.09.26-dd393fe", agentRuntimePackage: null, agentRuntimeVersion: null, - commandDigest: "sha256:1df2a15b93bc3a14fa47fa3315344ba023fe2412048047cdc6f32096a6336564", + commandDigest: "sha256:13207d7b6afcfe681d4fe9098655df056c2cf87e03f5141e961cd01c32c5bdd2", // 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", @@ -71,10 +71,10 @@ export const QUALIFIED_ACPX_PROFILES: Readonly< }, copilot: { driverKind: ACPX_DRIVER_KIND, protocolVersion: ACPX_DRIVER_PROTOCOL_VERSION, - acpxVersion: QUALIFIED_ACPX_VERSION, agent: "copilot", agentProfileVersion: 14, + acpxVersion: QUALIFIED_ACPX_VERSION, agent: "copilot", agentProfileVersion: 15, agentServerPackage: "@github/copilot", agentServerVersion: "1.0.88", agentRuntimePackage: null, agentRuntimeVersion: null, - commandDigest: "sha256:5e5955c57917a7e7c25703b3ed9e420cd72105eab033b611508dc37fdba3858b", + commandDigest: "sha256:8faf47520f156c62c79ea6ca7d1d90a85ddc9a6621f798bd14e87bc6c643bd04", // 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/src/drivers/acpx/recovery-identity.test.ts b/packages/paperclip-runner/src/drivers/acpx/recovery-identity.test.ts index 780b5514c6f..ae08bac786c 100644 --- a/packages/paperclip-runner/src/drivers/acpx/recovery-identity.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/recovery-identity.test.ts @@ -27,6 +27,9 @@ afterEach(async () => { describe("ACPX recovery identity", () => { it.each([ + ["cursor", "../../../test/fixtures/cursor-acp/profile-v10-identity.json"], + ["copilot", "../../../test/fixtures/copilot-profile-v14-identity.json"], + ["pi", "../../../test-fixtures/pi-acp/profile-v13-identity.json"], ["cursor", "../../../test/fixtures/cursor-acp/profile-v7-identity.json"], ["cursor", "../../../test/fixtures/cursor-acp/profile-v8-identity.json"], ["cursor", "../../../test/fixtures/cursor-acp/profile-v9-identity.json"], diff --git a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.context-control.test.ts b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.context-control.test.ts index ecc4c6167d3..cd9ee34129d 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.context-control.test.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.context-control.test.ts @@ -42,8 +42,30 @@ import { type PrpEvent, type PrpStructuredRunResult, } from "./codex-app-server-driver.test-support.js"; +import { rehydrateRunnerdItemNotification } from "../../live/runnerd-codex-transport.js"; describe("Codex app-server Codex driver", () => { + it("retains one correlation-bound steering acknowledgement after the rehydrated runnerd echo", async () => { + const transport = new FakeCodexTransport("thread-root"); + const session = await makeDriver([transport]).openSession({ + runId: "run-steering-echo", normalizedSessionId: "normalized-steering-echo", workingDirectory: WORKSPACE, + }); + const { turnId } = await session.startTurn({ message: { role: "user", text: "Start." } }); + await session.steer?.({ turnId, message: { role: "user", text: "Stay concise." }, correlationId: "queued-comment-1" }); + transport.push("item/completed", rehydrateRunnerdItemNotification({ + provider: "acpx", providerTurnId: turnId, itemId: `acpx-control-${"a".repeat(64)}`, + kind: "steering_acknowledgement", mode: "steer", status: "acknowledged", + text: "Steering acknowledged for the active turn.", + }, "thread-root", turnId)); + await new Promise(resolve => setTimeout(resolve, 0)); + await session.close({ reason: "fixture complete" }); + const events: PrpEvent[] = []; + for await (const event of session.events()) events.push(event); + expect(events.filter(event => event.eventType === "item.completed" && event.payload.kind === "steering_acknowledgement")) + .toEqual([expect.objectContaining({ turnId, itemId: `${turnId}:steer:queued-comment-1`, + payload: expect.objectContaining({ kind: "steering_acknowledgement", status: "acknowledged" }) })]); + }); + it("captures an exact skillless model/environment snapshot with credentials absent", async () => { const transport = new FakeCodexTransport(); const session = await makeDriver([transport]).openSession({ diff --git a/packages/paperclip-runner/src/drivers/codex/codex-session-notifications.ts b/packages/paperclip-runner/src/drivers/codex/codex-session-notifications.ts index 59370b07385..ddbfb21751d 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-session-notifications.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-session-notifications.ts @@ -331,12 +331,12 @@ async function mapNotificationBody(state: CodexSessionState, notification: Codex if ( notification.method === "item/completed" && text(params.kind) === "steering_acknowledgement" - && Object.keys(item).length === 0 ) { // runnerd persists its own command acknowledgement as a canonical PRP // item. The request() call is already the authoritative acknowledgement // and steer() emits the user-visible item with the active turn binding. - // Do not reinterpret this transport-level echo as an unbound Codex item. + // Rehydration adds an item object to this transport echo; it still must + // not become a second acknowledgement beside the correlation-bound item. return; } if (notification.method === "paperclip/runResult") { diff --git a/packages/paperclip-runner/src/live/live-session.test.ts b/packages/paperclip-runner/src/live/live-session.test.ts index 7b973bb7744..9860cafe1c6 100644 --- a/packages/paperclip-runner/src/live/live-session.test.ts +++ b/packages/paperclip-runner/src/live/live-session.test.ts @@ -43,8 +43,8 @@ it("admits Pi live sessions without candidate opt-in while preserving the exact const session = await service.create({ provider: "acpx", acpxAgent: "pi", piThinkingLevel: "low", requestedModel: "openrouter/deepseek/deepseek-v4-flash-0731" }); try { expect(session.snapshot().config.acpxProfile).toMatchObject({ - agent: "pi", agentProfileVersion: 13, - commandDigest: "sha256:fe1e6da01b2a9e4c691ca27cf689d2d6de846a93be6b23fc1e103c9addd7b177", + agent: "pi", agentProfileVersion: 14, + commandDigest: "sha256:f35145437eeb355ed37bc5a4fa93d7ede561d9c45daf311979b46890b808ddd4", }); await expect(service.create({ provider: "acpx", acpxAgent: "pi", piThinkingLevel: "low", requestedModel: "another-model" })) .rejects.toThrow("requires exact model"); diff --git a/packages/paperclip-runner/test-fixtures/pi-acp/profile-v14-identity.json b/packages/paperclip-runner/test-fixtures/pi-acp/profile-v14-identity.json new file mode 100644 index 00000000000..35314d2ed1e --- /dev/null +++ b/packages/paperclip-runner/test-fixtures/pi-acp/profile-v14-identity.json @@ -0,0 +1,43 @@ +{ + "commandDigest": "sha256:f35145437eeb355ed37bc5a4fa93d7ede561d9c45daf311979b46890b808ddd4", + "declaration": { + "schema": "paperclip.acpx_profile_declaration.v1", + "agent": "pi", + "agentProfileVersion": 14, + "acpxVersion": "0.13.1", + "agentServerVersion": "0.0.33", + "agentRuntimeVersion": "1.0.0", + "nodeVersion": "24.21.0", + "closure": { + "darwin-arm64": "f6538a35e08f1c1816e738277a1843acfa1ce4522bbaa3340dbddd859dc7dd04", + "darwin-x64": "4728e5a4e7fc1ba602c3e219824fb84884b05a06cdd19dd3e521ee312e1b373a", + "linux-x64": "2957c0ec20ca1ace64d1a2b10c4a99f47f59e0c5c33a89161c1d5b48341c2b25" + }, + "wrapperSha256": "8ef777e9e6a3cef37775bab2b800e4931330c23a0b151ce07fdc5b943f28435d", + "helperSha256": "2191b1e5f281d24508ec7eaff39011ee863a8a70bf54f4aa0490ad0172b323da", + "extensionSha256": "8f9b538dbe924a314099f9597ee3876f7ccd88211c4f4b2e52f46b6c7ffa76ba", + "agentFilesBinding": "registered-runtime-context-v1", + "nativeQuestions": "select-confirm-input-editor-v2", + "pathPolicy": "sdk-normalization-v1", + "mcpToolNames": "provider-safe-exact-source-v1", + "acpxPatchSha256": "c0139d0b3af085ec5272a7812c9f89aff0e8618d1d44d9c9e9a59525d1972436", + "nativeAssistantMessages": "pi1-rpc-deltas-v3-bound-message-and-tool-receipts", + "nativeNotices": "session-extension-v2-bounded-runtime-failure", + "messageProjectionSha256": "17d1e91b948dfa6a56e1905645add52b502041ad843c8d50a13b07956f22aadd", + "noticeProjectionSha256": "014485adc690998e395d334ea3ae67f72be6060a2fc78a1185df7b8d1da28fbf", + "sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1", + "nativeInputDisposition": "queued-only-v1", + "cacheWarming": "disabled-owned-decision-v1", + "thinkingLevel": { + "supported": [ + "off", + "low", + "high", + "max" + ], + "newConfigurationDefault": "low", + "admission": "explicit-native-effective-before-prompt-v1", + "recovery": "mode-bound-v1" + } + } +} diff --git a/packages/paperclip-runner/test/fixtures/copilot-profile-v15-identity.json b/packages/paperclip-runner/test/fixtures/copilot-profile-v15-identity.json new file mode 100644 index 00000000000..12ef2b3780d --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/copilot-profile-v15-identity.json @@ -0,0 +1,43 @@ +{ + "commandDigest": "sha256:8faf47520f156c62c79ea6ca7d1d90a85ddc9a6621f798bd14e87bc6c643bd04", + "declaration": { + "schema": "paperclip.acpx_profile_declaration.v1", + "agent": "copilot", + "agentProfileVersion": 15, + "acpxVersion": "0.13.1", + "agentServerVersion": "1.0.88", + "protocolPolicyRevision": "copilot-agent-manual-v1", + "acpxPatchSha256": "c0139d0b3af085ec5272a7812c9f89aff0e8618d1d44d9c9e9a59525d1972436", + "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": "b83898b2453d0f425275d3bc58140675fc8f14505703103079094093f16be43f", + "semanticDirectDriverSourceSha256": "2db465e56ba41f06b406e039e566d9e3aea0a014fc6a47a6a54d5bc682dabcab", + "semanticValidationSourceSha256": "039fca52266fe21ba74ca64660d21228314d8ed737b19ba67941bfb25086e4d5", + "semanticNormalizationSourceSha256": "b1ce59251bf9c671f66e0e5173646190b9a2d1bfc5bdb7fb123824213986b92b", + "semanticCompletionContractSourceSha256": "00a684f9aea87f68eabf0974681c52461bc308631a1252a89c4ed087d7158b28", + "semanticValidatorsSourceSha256": "950b49b233d8dd62ff858398becfc55f0a181160cb8a434d9221b5c17816867c", + "semanticSchemaBundleSourceSha256": "2974c75590f816fbdbc7806f531698fdc8d62a99601a17625b81de0f074d6383", + "semanticSidecarProtocolSourceSha256": "72d9800603a652d6580084bb3b5dac4d4dd6d644324b0d15d293d7c198fd6b55", + "semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1" + } +} diff --git a/packages/paperclip-runner/test/fixtures/cursor-acp/profile-v11-identity.json b/packages/paperclip-runner/test/fixtures/cursor-acp/profile-v11-identity.json new file mode 100644 index 00000000000..1b954a59a17 --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/cursor-acp/profile-v11-identity.json @@ -0,0 +1,59 @@ +{ + "commandDigest": "sha256:13207d7b6afcfe681d4fe9098655df056c2cf87e03f5141e961cd01c32c5bdd2", + "declaration": { + "schema": "paperclip.acpx_profile_declaration.v1", + "agent": "cursor", + "agentProfileVersion": 11, + "acpxVersion": "0.13.1", + "agentServerVersion": "2026.09.26-dd393fe", + "patchVersion": "paperclip-cursor-usage-v4", + "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": "257424bd48e35412091c6adfc61e4648e836757ec1d240d890bba81a24918c30", + "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": "6f28c799c5afdc64fbdff8a2157f565f17ae7615efe014ac389d63bf70cf2be2", + "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": "eadb8bb8ffb0450455b15b88c9b230307a9e149958a0c452d16dd157b4633d74", + "executable": "node", + "entrypoint": "index.js", + "vendorClosureSha256": "bf04ef8ea6a63191067a6b3e15139aa2c793d8419daab246aa3f0a012e1bbcd9" + } + }, + "patchVersion": "paperclip-cursor-usage-v4" + }, + "agentFilesBinding": "registered-runtime-context-v1", + "instructionBinding": "native-global-rules-v1", + "instructionAdmission": "synchronous-protocol-guard-v1", + "acpxPatchSha256": "c0139d0b3af085ec5272a7812c9f89aff0e8618d1d44d9c9e9a59525d1972436", + "sessionModeAdmission": "native-config-ack-recovery-bound-v1", + "sessionModes": [ + "agent", + "plan", + "ask" + ], + "defaultSessionMode": "agent", + "sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1", + "nativePlanToolIdentity": "request-item-id-bound-lifecycle-v1", + "nativePermissionToolIdentity": "opaque-native-tool-id-sha256-v1", + "toolIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1", + "permissionAdapterSourceSha256": "49421a779c76e286ce65d75052026eab9630b1995af9ccf045a2d43d284633ce", + "toolEvidenceSourceSha256": "8310308ed9605415cb28b7c350d83defef68e90cd30bc2f0491d3d09825e8cd9" + } +} diff --git a/patches/acpx@0.13.1.patch b/patches/acpx@0.13.1.patch index 97a0d3257d8..caf1ef78d98 100644 --- a/patches/acpx@0.13.1.patch +++ b/patches/acpx@0.13.1.patch @@ -20,10 +20,9 @@ index 5e2113ac0..45aff03c3 100644 private setSessionModelThroughConfig; private setSessionModelThroughLegacyMethod; diff --git a/dist/live-checkpoint-BSIrfgVo.js b/dist/live-checkpoint-BSIrfgVo.js -index d454fd7c5..6eba74345 100644 --- a/dist/live-checkpoint-BSIrfgVo.js +++ b/dist/live-checkpoint-BSIrfgVo.js -@@ -1068,12 +1068,14 @@ function serializeSessionRecordForDisk(record) { +@@ -1068,12 +1068,14 @@ last_agent_disconnect_reason: canonical.lastAgentDisconnectReason, protocol_version: canonical.protocolVersion, agent_capabilities: canonical.agentCapabilities, @@ -38,7 +37,7 @@ index d454fd7c5..6eba74345 100644 acpx: canonical.acpx, imported_from: canonical.importedFrom ? { record_id: canonical.importedFrom.recordId, -@@ -1239,7 +1241,8 @@ function parseRequestTokenUsage(raw) { +@@ -1239,7 +1241,8 @@ for (const [key, value] of Object.entries(record)) { const parsed = parseTokenUsage(value); if (parsed == null) return null; @@ -48,7 +47,7 @@ index d454fd7c5..6eba74345 100644 } return usage; } -@@ -1325,6 +1328,93 @@ function isAgentMessage$1(raw) { +@@ -1325,6 +1328,93 @@ function isConversationMessage(raw) { return raw === "Resume" || isUserMessage$1(raw) || isAgentMessage$1(raw); } @@ -142,7 +141,7 @@ index d454fd7c5..6eba74345 100644 function parseConversationRecord(record) { if (!hasValidConversationCore(record)) return; const title = parseConversationTitle(record.title); -@@ -1339,7 +1429,8 @@ function parseConversationRecord(record) { +@@ -1339,7 +1429,8 @@ updated_at: record.updated_at, cumulative_token_usage: cumulativeTokenUsage ?? {}, cumulative_cost: cumulativeCost, @@ -152,7 +151,7 @@ index d454fd7c5..6eba74345 100644 }; } const INVALID_VALUE = Symbol("invalid"); -@@ -1541,12 +1632,14 @@ function parseSessionRecord(raw) { +@@ -1541,12 +1632,14 @@ lastAgentDisconnectReason: optionals.lastAgentDisconnectReason, protocolVersion: typeof record.protocol_version === "number" ? record.protocol_version : void 0, agentCapabilities: asRecord$4(record.agent_capabilities), @@ -167,7 +166,7 @@ index d454fd7c5..6eba74345 100644 acpx: parseAcpxState(record.acpx), importedFrom: metadata.importedFrom }; -@@ -1661,8 +1754,9 @@ const ZED_TAG_KEYS = /* @__PURE__ */ new Set([ +@@ -1661,8 +1754,9 @@ "RedactedThinking", "ToolUse" ]); @@ -178,7 +177,7 @@ index d454fd7c5..6eba74345 100644 "agent_capabilities", "messages.Agent.content.ToolUse.input", "acpx.desired_config_options", -@@ -3104,10 +3198,10 @@ function authEnvKey(methodId) { +@@ -3104,10 +3198,10 @@ authEnvKeyCache.set(methodId, key); return key; } @@ -191,7 +190,7 @@ index d454fd7c5..6eba74345 100644 if (typeof value === "string" && value.trim().length > 0) return value; } function protectedEnvKey(key) { -@@ -3135,8 +3229,21 @@ function promotePrefixedAuthEnvironment(env) { +@@ -3135,8 +3229,21 @@ } return protectedKeys; } @@ -215,7 +214,7 @@ index d454fd7c5..6eba74345 100644 const protectedAuthEnvKeys = promotePrefixedAuthEnvironment(env); if (authCredentials) for (const [methodId, credential] of Object.entries(authCredentials)) { addAuthCredentialEnvKeys(protectedAuthEnvKeys, methodId, credential); -@@ -3178,10 +3285,10 @@ function resolveConfiguredAuthCredential(methodId, authCredentials) { +@@ -3178,10 +3285,10 @@ const configCredentials = authCredentials ?? {}; return configCredentials[methodId] ?? configCredentials[toEnvToken(methodId)]; } @@ -228,7 +227,7 @@ index d454fd7c5..6eba74345 100644 stdio: [ "pipe", "pipe", -@@ -3961,10 +4068,13 @@ function resolveClientCapabilities(params) { +@@ -3961,10 +4068,13 @@ ...params.elicitationModes.includes("url") ? { url: {} } : {} } } : {} }; @@ -244,7 +243,7 @@ index d454fd7c5..6eba74345 100644 }; } function hasResponseField(response, field) { -@@ -4067,6 +4177,27 @@ function createNdJsonMessageStream(agentCommand, output, input) { +@@ -4067,6 +4177,27 @@ } }) }; } @@ -272,7 +271,7 @@ index d454fd7c5..6eba74345 100644 var AcpClient = class { options; connection; -@@ -4105,7 +4236,9 @@ var AcpClient = class { +@@ -4105,7 +4236,9 @@ cwd: asAbsoluteCwd(options.cwd), authPolicy: options.authPolicy ?? "skip", permissionPolicy: snapshotPermissionPolicy(options.permissionPolicy), @@ -283,7 +282,7 @@ index d454fd7c5..6eba74345 100644 }; this.eventHandlers = { onAcpMessage: this.options.onAcpMessage, -@@ -4216,9 +4349,21 @@ var AcpClient = class { +@@ -4216,9 +4349,21 @@ this.lastAgentExit = void 0; this.lastKnownPid = child.pid ?? void 0; this.attachAgentLifecycleObservers(child); @@ -305,7 +304,7 @@ index d454fd7c5..6eba74345 100644 if (!this.options.verbose) return; process.stderr.write(chunk); }); -@@ -4226,6 +4371,7 @@ var AcpClient = class { +@@ -4226,6 +4371,7 @@ const output = Readable.toWeb(child.stdout); const stream = this.createTappedStream(createNdJsonMessageStream(this.options.agentCommand, input, output)); const connection = this.createConnection(stream, launch); @@ -313,7 +312,7 @@ index d454fd7c5..6eba74345 100644 connection.signal.addEventListener("abort", () => { this.recordAgentExit("connection_close", child.exitCode ?? null, child.signalCode ?? null); }, { once: true }); -@@ -4253,7 +4399,12 @@ var AcpClient = class { +@@ -4253,7 +4399,12 @@ geminiAcp: isGeminiAcpCommand(spawnCommand, args), copilotAcp: isCopilotAcpCommand(spawnCommand, args), claudeAcp: isClaudeAcpCommand(spawnCommand, args), @@ -327,7 +326,7 @@ index d454fd7c5..6eba74345 100644 }; } logAgentLaunch(plan) { -@@ -4279,10 +4430,17 @@ var AcpClient = class { +@@ -4279,10 +4430,17 @@ } async spawnAgentProcess(plan) { const spawnCommand = buildAgentSpawnCommand(plan.spawnCommand, plan.args, process.platform, plan.spawnOptions.env); @@ -347,7 +346,7 @@ index d454fd7c5..6eba74345 100644 try { await waitForSpawn$1(spawnedChild); } catch (error) { -@@ -4293,10 +4451,10 @@ var AcpClient = class { +@@ -4293,10 +4451,10 @@ createConnection(stream, launch) { const app = client({ name: "acpx" }).onNotification(methods.client.session.update, async ({ params }) => { await this.handleSessionUpdate(params); @@ -361,7 +360,7 @@ index d454fd7c5..6eba74345 100644 }).onRequest(methods.client.fs.readTextFile, async ({ params }) => { return await this.handleReadTextFile(params); }).onRequest(methods.client.fs.writeTextFile, async ({ params }) => { -@@ -4312,6 +4470,11 @@ var AcpClient = class { +@@ -4312,6 +4470,11 @@ }).onRequest(methods.client.terminal.release, async ({ params }) => { return await this.handleReleaseTerminal(params); }); @@ -373,7 +372,7 @@ index d454fd7c5..6eba74345 100644 if (launch.devinAcp) app.onRequest("_cognition.ai/request_diagnostics", (params) => { return params && typeof params === "object" && !Array.isArray(params) ? params : {}; }, async () => ({})); -@@ -4333,12 +4496,12 @@ var AcpClient = class { +@@ -4333,12 +4496,12 @@ async initializeProtocolConnection(connection, launch) { const initializePromise = connection.initialize({ protocolVersion: PROTOCOL_VERSION, @@ -388,7 +387,7 @@ index d454fd7c5..6eba74345 100644 clientInfo: resolveClientInfo(launch.devinAcp) }); const initialized = launch.geminiAcp ? await withTimeout(initializePromise, resolveGeminiAcpStartupTimeoutMs()) : await initializePromise; -@@ -4358,6 +4521,49 @@ var AcpClient = class { +@@ -4358,6 +4521,49 @@ throw normalizedError; } createTappedStream(base) { @@ -438,7 +437,7 @@ index d454fd7c5..6eba74345 100644 const onAcpMessage = () => this.eventHandlers.onAcpMessage; const onAcpOutputMessage = () => this.eventHandlers.onAcpOutputMessage; const elicitationRequestIds = /* @__PURE__ */ new Set(); -@@ -4367,8 +4573,12 @@ var AcpClient = class { +@@ -4367,8 +4573,12 @@ const shouldSuppressInboundReplaySessionUpdate = (message) => { return this.suppressReplaySessionUpdateMessages && isSessionUpdateNotification(message); }; @@ -452,7 +451,7 @@ index d454fd7c5..6eba74345 100644 if (requestId !== void 0) { elicitationRequestIds.add(requestId); return; -@@ -4379,6 +4589,7 @@ var AcpClient = class { +@@ -4379,6 +4589,7 @@ } }; return { @@ -460,7 +459,7 @@ index d454fd7c5..6eba74345 100644 readable: new ReadableStream({ async start(controller) { const reader = base.readable.getReader(); try { -@@ -4386,16 +4597,27 @@ var AcpClient = class { +@@ -4386,16 +4597,27 @@ const { value, done } = await reader.read(); if (done) break; if (!value) continue; @@ -490,7 +489,7 @@ index d454fd7c5..6eba74345 100644 const promptOwner = promptRequestOwner(message); if (promptOwner) bindPromptOwner(promptOwner); const id = responseId(message); -@@ -4403,10 +4625,20 @@ var AcpClient = class { +@@ -4403,10 +4625,20 @@ onAcpOutputMessage()?.("outbound", message); onAcpMessage()?.("outbound", message); } @@ -511,7 +510,7 @@ index d454fd7c5..6eba74345 100644 writer.releaseLock(); } } }) -@@ -4422,7 +4654,10 @@ var AcpClient = class { +@@ -4422,7 +4654,10 @@ const createPromise = this.runConnectionRequest(() => connection.newSession({ cwd: sessionCwd, mcpServers: this.options.mcpServers ?? [], @@ -523,21 +522,21 @@ index d454fd7c5..6eba74345 100644 })); result = claudeAcp ? await withTimeout(createPromise, resolveClaudeAcpSessionCreateTimeoutMs()) : await createPromise; } catch (error) { -@@ -4596,6 +4831,13 @@ var AcpClient = class { +@@ -4589,6 +4824,13 @@ + } catch (error) { + throw maybeWrapSessionControlError("session/set_config_option", error, `for "${configId}"="${value}"`); } - } ++ } + async requestExtension(method, params) { + closedExtensionMethods([method]); + const payload = boundedExtensionRecord(params); + const connection = this.getConnection(); + const result = await this.runConnectionRequest(() => connection.extMethod(method, payload)); + return boundedExtensionRecord(result); -+ } + } async setSessionModel(sessionId, modelId, controlOverride) { const control = this.resolveModelControl(sessionId, controlOverride); - if (!control) throw new RequestedModelUnsupportedError(`Cannot set model "${modelId}": the ACP session did not advertise a model config option or legacy session/set_model support.`, "missing-capability"); - const resolvedModelId = resolveRequestedModelId({ -@@ -4864,7 +5106,7 @@ var AcpClient = class { +@@ -4864,7 +5106,7 @@ } selectAuthMethod(methods) { for (const method of methods) { @@ -546,7 +545,7 @@ index d454fd7c5..6eba74345 100644 if (envCredential) return { methodId: method.id, credential: envCredential, -@@ -4890,7 +5125,7 @@ var AcpClient = class { +@@ -4890,7 +5132,7 @@ } readAgentSpecificEnvCredential(methodId) { if (!this.isGrokBuildAcpCommand() || methodId !== "xai.api_key") return; @@ -555,7 +554,7 @@ index d454fd7c5..6eba74345 100644 return typeof value === "string" && value.trim().length > 0 ? value : void 0; } selectAgentManagedAuthMethod(methodId) { -@@ -4915,9 +5150,9 @@ var AcpClient = class { +@@ -4915,9 +5157,9 @@ await connection.authenticate({ methodId: selected.methodId }); this.log(`authenticated with method ${selected.methodId} (${selected.source})`); } @@ -567,7 +566,7 @@ index d454fd7c5..6eba74345 100644 if (hostResponse) return hostResponse; const { response, recorded } = await this.resolvePermissionRequestFromMode(params); if (!recorded) { -@@ -4926,14 +5161,81 @@ var AcpClient = class { +@@ -4926,14 +5168,81 @@ } return response; } @@ -651,7 +650,7 @@ index d454fd7c5..6eba74345 100644 })).then((response) => ({ kind: "response", response -@@ -4970,7 +5272,7 @@ var AcpClient = class { +@@ -4970,7 +5279,7 @@ isElicitationSessionCancelling(sessionId) { return this.closing || this.cancellingSessionIds.has(sessionId); } @@ -660,7 +659,7 @@ index d454fd7c5..6eba74345 100644 if (!this.options.onPermissionRequest) return; const signal = this.cancellationSignalForSession(params.sessionId); try { -@@ -4978,7 +5280,7 @@ var AcpClient = class { +@@ -4978,7 +5287,7 @@ sessionId: params.sessionId, raw: params, inferredKind: inferToolKind(params) @@ -669,7 +668,7 @@ index d454fd7c5..6eba74345 100644 return this.hostPermissionDecisionResponse(params, signal, decision); } catch (error) { return this.hostPermissionErrorResponse(params, signal, error); -@@ -5028,6 +5330,12 @@ var AcpClient = class { +@@ -5028,6 +5337,12 @@ attachAgentLifecycleObservers(child) { child.once("exit", (exitCode, signal) => { this.recordAgentExit("process_exit", exitCode, signal); @@ -682,7 +681,7 @@ index d454fd7c5..6eba74345 100644 }); child.once("close", (exitCode, signal) => { this.recordAgentExit("process_close", exitCode, signal); -@@ -5239,6 +5547,7 @@ function applyConversation(record, conversation) { +@@ -5239,6 +5554,7 @@ record.cumulative_token_usage = conversation.cumulative_token_usage; record.cumulative_cost = conversation.cumulative_cost; record.request_token_usage = conversation.request_token_usage; @@ -690,7 +689,7 @@ index d454fd7c5..6eba74345 100644 } //#endregion //#region src/runtime/engine/session-options.ts -@@ -5701,7 +6010,8 @@ function cloneSessionConversation(conversation) { +@@ -5701,7 +6017,8 @@ updated_at: conversation.updated_at, cumulative_token_usage: deepClone(conversation.cumulative_token_usage ?? {}), cumulative_cost: cloneUsageCost(conversation.cumulative_cost), @@ -700,7 +699,7 @@ index d454fd7c5..6eba74345 100644 }; } function cloneUsageCost(cost) { -@@ -5771,10 +6081,20 @@ function recordSessionUpdate(conversation, state, notification, timestamp = isoN +@@ -5771,10 +6088,20 @@ trimConversationForRuntime(conversation); return acpx; } @@ -723,7 +722,7 @@ index d454fd7c5..6eba74345 100644 updateConversationTimestamp(conversation, timestamp); trimConversationForRuntime(conversation); return true; -@@ -6435,6 +6755,27 @@ async function withConnectedSession(options) { +@@ -6435,6 +6762,27 @@ //#region src/runtime/engine/prompt-turn.ts const SESSION_REPLY_IDLE_MS = 1e3; const SESSION_REPLY_DRAIN_TIMEOUT_MS = 5e3; @@ -751,7 +750,7 @@ index d454fd7c5..6eba74345 100644 async function runPromptTurn(params) { try { const promptPromise = params.client.prompt(params.sessionId, params.prompt, params.onPromptRequestStarted, params.onElicitation); -@@ -6444,7 +6785,23 @@ async function runPromptTurn(params) { +@@ -6444,7 +6792,23 @@ idleMs: SESSION_REPLY_IDLE_MS, timeoutMs: SESSION_REPLY_DRAIN_TIMEOUT_MS }).catch(() => {}); diff --git a/scripts/pi-public-install-probe.mjs b/scripts/pi-public-install-probe.mjs index 0a55967cd0c..6750c4bbafd 100644 --- a/scripts/pi-public-install-probe.mjs +++ b/scripts/pi-public-install-probe.mjs @@ -20,8 +20,8 @@ assert.equal(JSON.parse(await readFile(join(server, 'package.json'), 'utf8')).na const { resolvePaperclipRunnerBinary } = await import(pathToFileURL(join(server, 'dist/services/native-runtime/native-codex-runner.js'))); const { createCapabilityRunnerdCodexTransport } = await import(pathToFileURL(join(server, 'dist/vendor/paperclip-runner/live/runnerd-codex-transport.js'))); const { QUALIFIED_ACPX_PROFILES } = await import(pathToFileURL(join(server, 'dist/vendor/paperclip-runner/drivers/acpx/qualified-profiles.js'))); -assert.equal(QUALIFIED_ACPX_PROFILES.pi.agentProfileVersion, 13); -assert.equal(QUALIFIED_ACPX_PROFILES.pi.commandDigest, 'sha256:fe1e6da01b2a9e4c691ca27cf689d2d6de846a93be6b23fc1e103c9addd7b177'); +assert.equal(QUALIFIED_ACPX_PROFILES.pi.agentProfileVersion, 14); +assert.equal(QUALIFIED_ACPX_PROFILES.pi.commandDigest, 'sha256:f35145437eeb355ed37bc5a4fa93d7ede561d9c45daf311979b46890b808ddd4'); assert.equal(QUALIFIED_ACPX_PROFILES.pi.qualificationModel, 'openrouter/deepseek/deepseek-v4-flash-0731'); const daemon = resolvePaperclipRunnerBinary(); assert.equal(await realpath(daemon), join(server, 'dist/vendor/paperclip-runner/bin/paperclip-runnerd')); @@ -63,7 +63,7 @@ try { assert.equal(state.activeTurnId, null); assert.equal(state.identity, null); assert.equal(state.providerExitUnconfirmed, false); - console.log(JSON.stringify({ schema: 'paperclip.pi.public-npm-install.v1', target: `${process.platform}-${process.arch}`, normalPackagedDaemon: true, exactPiProfile: 13, runtime: '1.0.0', credentials: 'none', promptCalls: 0, settledMs, cleanRunnerExit: true })); + console.log(JSON.stringify({ schema: 'paperclip.pi.public-npm-install.v1', target: `${process.platform}-${process.arch}`, normalPackagedDaemon: true, exactPiProfile: 14, runtime: '1.0.0', credentials: 'none', promptCalls: 0, settledMs, cleanRunnerExit: true })); } finally { try { await bundle?.transport.close(); } finally { clearTimeout(watchdog); await rm(root, { recursive: true, force: true }); }