diff --git a/packages/paperclip-runner/src/contracts/native-execution.test.ts b/packages/paperclip-runner/src/contracts/native-execution.test.ts index 615101b0e28..e2f3075dc15 100644 --- a/packages/paperclip-runner/src/contracts/native-execution.test.ts +++ b/packages/paperclip-runner/src/contracts/native-execution.test.ts @@ -1,6 +1,7 @@ import { nativeTaskConstraints } from "../backends/runtime-context.js"; import { createHash } from "node:crypto"; import { QUALIFIED_ACPX_PROFILES } from "../drivers/acpx/qualified-profiles.js"; +import { QUALIFIED_ACPX_VERSION } from "../drivers/acpx/generated-profiles.js"; import { describe, expect, it } from "vitest"; import { buildNativeModelEnvelope, parseNativeExecutionInput, NATIVE_EXECUTION_INPUT_SCHEMA, NATIVE_EXECUTION_INPUT_SCHEMA_V6, NATIVE_EXECUTION_INPUT_SCHEMA_V7, NATIVE_EXECUTION_INPUT_SCHEMA_V9, NATIVE_EXECUTION_INPUT_SCHEMA_V10, type NativeExecutionInputV1 } from "./native-execution.js"; @@ -450,6 +451,37 @@ describe("NativeExecutionInputV1", () => { expect(parseNativeExecutionInput(parsed)).toEqual(parsed); }); + it("decodes a Claude input saved under ACPX 0.13.1 as the current qualified profile", () => { + const savedProfile = { + agent: "claude", + driverKind: "acpx_runtime", + acpxVersion: "0.13.1", + commandDigest: "sha256:9d73d1f0f121fb96cc8badb28c22d5bff02d8582eb2e40360a81c189e1b9422a", + protocolVersion: 1, + agentServerPackage: "@agentclientprotocol/claude-agent-acp", + agentServerVersion: "0.73.0", + agentProfileVersion: 1, + agentRuntimePackage: "@anthropic-ai/claude-agent-sdk", + agentRuntimeVersion: "0.3.286", + }; + const saved = { + ...input, + session: { ...input.session, driverKind: "acpx_runtime" }, + provider: { + kind: "acpx", agent: "claude", model: "claude-opus-5-5", + permissionPolicy: "interactive", profile: savedProfile, + }, + }; + const parsed = parseNativeExecutionInput(saved); + expect(parsed.provider).toEqual({ ...saved.provider, profile: { ...savedProfile, acpxVersion: QUALIFIED_ACPX_VERSION } }); + expect(parseNativeExecutionInput(parsed)).toEqual(parsed); + for (const acpxVersion of ["0.13.0", "0.21.9", "0.23.0", "", null, 13]) { + expect(() => parseNativeExecutionInput({ + ...saved, provider: { ...saved.provider, profile: { ...savedProfile, acpxVersion } }, + })).toThrow("qualified ACPX v1 profile"); + } + }); + it.each([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40] as const)("decodes closed Copilot profile history at version %s without admitting a future version", (agentProfileVersion) => { const { permissionPolicy, qualificationStatus: _qualificationStatus, ...snapshot } = QUALIFIED_ACPX_PROFILES.copilot; const provider = { diff --git a/packages/paperclip-runner/src/contracts/native-execution.ts b/packages/paperclip-runner/src/contracts/native-execution.ts index 4ce817b79d5..68fbd3628b8 100644 --- a/packages/paperclip-runner/src/contracts/native-execution.ts +++ b/packages/paperclip-runner/src/contracts/native-execution.ts @@ -4,7 +4,7 @@ import { resolveGrokEffort } from "../drivers/acpx/grok-effort.js"; import { resolveClaudeEffort } from "../drivers/acpx/claude-effort.js"; import type { DotBindingSnapshot, MuseBindingSnapshot, ExternalBotProviderKind } from "./external-provider.js"; import { QUALIFIED_ACPX_VERSION, QUALIFIED_ACPX_PROFILE_DATA } from "../drivers/acpx/generated-profiles.js"; -import { isSupportedAcpxProfileVersion, type AcpxProfileVersion } from "../drivers/acpx/profile-compatibility.js"; +import { isDecodableAcpxVersion, isSupportedAcpxProfileVersion, type AcpxProfileVersion } from "../drivers/acpx/profile-compatibility.js"; import { isProviderMode } from "./provider-mode.js"; import { createHash } from "node:crypto"; import type { PrpStructuredRunResult, PrpTerminalState } from "../protocol/replay-contract.js"; @@ -754,7 +754,7 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput if ( profile.driverKind !== "acpx_runtime" || profile.protocolVersion !== 1 - || profile.acpxVersion !== QUALIFIED_ACPX_VERSION + || !isDecodableAcpxVersion(profile.acpxVersion) || profile.agent !== provider.agent || !isSupportedAcpxProfileVersion(provider.agent, profile.agentProfileVersion) ) { diff --git a/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.ts b/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.ts index 2ec9855435d..22f4300e332 100644 --- a/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.ts +++ b/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.ts @@ -1,4 +1,4 @@ -import { QUALIFIED_ACPX_PROFILE_DATA } from "./generated-profiles.js"; +import { QUALIFIED_ACPX_PROFILE_DATA, QUALIFIED_ACPX_VERSION } from "./generated-profiles.js"; /** Decodable profile revisions, not permission to launch them. * Exact current profile/package/digest admission is checked separately. @@ -15,3 +15,9 @@ export function isSupportedAcpxProfileVersion(agent: string, value: unknown): va const latest = Math.max(current, Object.hasOwn(RETAINED_THROUGH, agent) ? RETAINED_THROUGH[agent]! : 0); return (value as number) >= 1 && (value as number) <= latest; } + +const DECODABLE_ACPX_VERSIONS: ReadonlySet = new Set(["0.13.1", QUALIFIED_ACPX_VERSION]); + +export function isDecodableAcpxVersion(value: unknown): boolean { + return DECODABLE_ACPX_VERSIONS.has(value); +}