mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 02:26:12 +02:00
fix(runner): fence warm ACPX owners on runtime contract changes
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
d6ff17fd0d
commit
951b06d08a
17 files changed
+194
-45
No files matched your search
@@ -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`,
|
||||
|
||||
@@ -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 } = {}) {
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ function record(value: unknown): Record<string, unknown> {
|
||||
/** 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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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")}`;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in new issue
Block a user