From ec0991d4ba3a9d0d503dd9593c2a80b51afbcb59 Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 29 Sep 2026 10:07:49 -0500 Subject: [PATCH] Admit current ACP profiles and verify cross-language pin agreement Co-Authored-By: Paperclip --- .../src/contracts/native-execution.test.ts | 25 +++++++++++++++++-- .../src/contracts/native-execution.ts | 4 +-- .../drivers/acpx/qualified-profiles.test.ts | 16 ++++++++++++ .../src/drivers/acpx/qualified-profiles.ts | 4 +-- 4 files changed, 43 insertions(+), 6 deletions(-) diff --git a/packages/paperclip-runner/src/contracts/native-execution.test.ts b/packages/paperclip-runner/src/contracts/native-execution.test.ts index 6599724252..ed082d720d 100644 --- a/packages/paperclip-runner/src/contracts/native-execution.test.ts +++ b/packages/paperclip-runner/src/contracts/native-execution.test.ts @@ -1,4 +1,5 @@ import { createHash } from "node:crypto"; +import { QUALIFIED_ACPX_PROFILES } from "../drivers/acpx/qualified-profiles.js"; import { describe, expect, it } from "vitest"; import { buildNativeModelEnvelope, parseNativeExecutionInput, NATIVE_EXECUTION_INPUT_SCHEMA, type NativeExecutionInputV1 } from "./native-execution.js"; @@ -280,7 +281,7 @@ describe("NativeExecutionInputV1", () => { })).toThrow("eventExpiryDays"); }); - it.each([1, 2, 3, 4, 5] 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] as const)("accepts only a closed ACPX profile matching the driver and agent at profile version %s", (agentProfileVersion) => { const provider = { kind: "acpx", agent: "pi", @@ -312,7 +313,7 @@ describe("NativeExecutionInputV1", () => { profile: provider.profile, }); expect(parseNativeExecutionInput(parsed)).toEqual(parsed); - for (const unsupportedVersion of [0, 6, 1.5, "5", null]) { + for (const unsupportedVersion of [0, 7, 1.5, "6", null]) { expect(() => parseNativeExecutionInput({ ...input, session: { ...input.session, driverKind: "acpx_runtime" }, @@ -332,6 +333,26 @@ describe("NativeExecutionInputV1", () => { })).toThrow("does not match"); }); + it.each(Object.values(QUALIFIED_ACPX_PROFILES))( + "admits the current $agent profile through the native execution boundary", + (declaration) => { + const { qualificationModel, reportedModelId: _reported, permissionPolicy, + modelPolicy: _modelPolicy, qualificationStatus: _status, ...profile } = declaration; + const provider = { + kind: "acpx", agent: declaration.agent, + model: qualificationModel || "explicit-provider-model", + permissionPolicy, profile, + }; + const parsed = parseNativeExecutionInput({ + ...input, + session: { ...input.session, driverKind: "acpx_runtime" }, + provider, + }); + expect(parsed.provider).toEqual(provider); + expect(parseNativeExecutionInput(parsed)).toEqual(parsed); + }, + ); + it("defaults legacy lifecycle state to per-turn and validates warm timeouts", () => { const legacy = structuredClone(input) as Record; delete (legacy.session as Record).lifecyclePolicy; diff --git a/packages/paperclip-runner/src/contracts/native-execution.ts b/packages/paperclip-runner/src/contracts/native-execution.ts index 2d1ec54a8b..157ce11f22 100644 --- a/packages/paperclip-runner/src/contracts/native-execution.ts +++ b/packages/paperclip-runner/src/contracts/native-execution.ts @@ -90,7 +90,7 @@ export interface NativeAcpxProfileSnapshot { protocolVersion: 1; acpxVersion: "0.13.1"; agent: NativeAcpxAgent; - agentProfileVersion: 1 | 2 | 3 | 4 | 5; + agentProfileVersion: 1 | 2 | 3 | 4 | 5 | 6; agentServerPackage: string; agentServerVersion: string; agentRuntimePackage: string | null; @@ -596,7 +596,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 !== 1 && profile.agentProfileVersion !== 2 && profile.agentProfileVersion !== 3 && profile.agentProfileVersion !== 4 && profile.agentProfileVersion !== 5 && profile.agentProfileVersion !== 6) ) { throw new NativeExecutionInputError("input.provider.profile does not match the qualified ACPX v1 profile"); } 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 9cde5dd0a9..2716e9e336 100644 --- a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "vitest"; +import { readFileSync } from "node:fs"; import { QUALIFIED_ACPX_PROFILES, @@ -6,6 +7,21 @@ import { } from "./qualified-profiles.js"; describe("qualified ACPX profiles", () => { + it.each(Object.values(QUALIFIED_ACPX_PROFILES))( + "keeps the Rust admission digest synchronized with the current $agent profile", + (profile) => { + const rust = readFileSync(new URL("../../../runner/crates/runner-core/src/acpx_provider_backend.rs", import.meta.url), "utf8"); + const start = rust.indexOf(`"${profile.agent}" => (`); + expect(start).toBeGreaterThan(0); + const end = rust.indexOf("\n ),", start); + expect(end).toBeGreaterThan(start); + const admission = rust.slice(start, end); + expect(admission).toContain(JSON.stringify(profile.agentServerPackage)); + expect(admission).toContain(JSON.stringify(profile.agentServerVersion)); + expect(admission).toContain(JSON.stringify(profile.commandDigest)); + }, + ); + it("binds each agent to one immutable package and model declaration", () => { for (const agent of ["pi", "claude", "codex", "grok"] as const) { const profile = QUALIFIED_ACPX_PROFILES[agent]; diff --git a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts index b248a44e81..f198a37507 100644 --- a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts +++ b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts @@ -2,7 +2,7 @@ export const QUALIFIED_ACPX_VERSION = "0.13.1" as const; export const ACPX_DRIVER_KIND = "acpx_runtime" as const; export const ACPX_DRIVER_PROTOCOL_VERSION = 1 as const; -import type { NativeAcpxAgent } from "../../contracts/native-execution.js"; +import type { NativeAcpxAgent, NativeAcpxProfileSnapshot } from "../../contracts/native-execution.js"; export type QualifiedAcpxAgent = NativeAcpxAgent; @@ -11,7 +11,7 @@ export interface QualifiedAcpxProfile { readonly protocolVersion: typeof ACPX_DRIVER_PROTOCOL_VERSION; readonly acpxVersion: typeof QUALIFIED_ACPX_VERSION; readonly agent: QualifiedAcpxAgent; - readonly agentProfileVersion: 1 | 2 | 3 | 4 | 5 | 6; + readonly agentProfileVersion: NativeAcpxProfileSnapshot["agentProfileVersion"]; readonly qualificationStatus?: "pending"; readonly modelPolicy?: "explicit-provider-verified"; /** Wire identity: an npm package name or a runner-owned builtin: identifier. */