From 5835c9867580bf887f16f39be6ad0ef8a05a6da8 Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 2 Sep 2026 23:57:10 -0500 Subject: [PATCH] fix(runner): preserve ACPX command attestation --- .../src/cli/acpx-runtime-sidecar.ts | 20 +++++++++++++---- .../drivers/acpx/recovery-identity.test.ts | 22 +++++-------------- .../src/drivers/acpx/recovery-identity.ts | 14 +++++++++--- .../src/drivers/acpx/runtime-host.ts | 2 +- 4 files changed, 34 insertions(+), 24 deletions(-) diff --git a/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts b/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts index e5d8ddac31..cecbd4560f 100644 --- a/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts +++ b/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts @@ -283,7 +283,10 @@ async function dispatch( startedAt: new Date().toISOString(), }); return { - identity: acpxProviderSessionIdentity(openedHost.identity()), + identity: acpxProviderSessionIdentity( + openedHost.identity(), + openedHost.binding(), + ), sidecarPid: process.pid, status: opened.status, }; @@ -398,7 +401,10 @@ async function dispatch( if (request.command === "session.read") { const activeHost = requireHost(); return { - identity: acpxProviderSessionIdentity(activeHost.identity()), + identity: acpxProviderSessionIdentity( + activeHost.identity(), + activeHost.binding(), + ), status: sanitizeRuntimeStatus( await readSidecarHostStatusWithin(activeHost), ), @@ -407,7 +413,10 @@ async function dispatch( if (request.command === "session.snapshot") { const activeHost = requireHost(); return { - identity: acpxProviderSessionIdentity(activeHost.identity()), + identity: acpxProviderSessionIdentity( + activeHost.identity(), + activeHost.binding(), + ), status: sanitizeRuntimeStatus( await readSidecarHostStatusWithin(activeHost), ), @@ -426,7 +435,10 @@ async function dispatch( // still serialized, and retainActiveHostCleanup keeps admission closed // until one sequential close proves ownership was released. const activeHost = requireHost({ allowCleanupRetry: true }); - const identity = acpxProviderSessionIdentity(activeHost.identity()); + const identity = acpxProviderSessionIdentity( + activeHost.identity(), + activeHost.binding(), + ); await closeSidecarHostForCommand( activeHost, boundedOptionalText(request.params.reason, "Paperclip suspension", 4_000), 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 f295da359b..12cda7b1d7 100644 --- a/packages/paperclip-runner/src/drivers/acpx/recovery-identity.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/recovery-identity.test.ts @@ -42,13 +42,13 @@ describe("ACPX recovery identity", () => { normalizedSessionId: "session-1", permissionMode: "approve-reads", }); - expect(acpxProviderSessionIdentity(record)).toEqual({ + expect(acpxProviderSessionIdentity(record, fixture.binding)).toEqual({ kind: "acpx", normalizedSessionId: "session-1", acpxRecordId: fixture.expected.acpxRecordId, backendSessionId: fixture.expected.backendSessionId, agentSessionId: fixture.expected.agentSessionId, - profileDigest: fixture.binding.profileDigest, + profileDigest: fixture.binding.commandDigest, workspaceDigest: fixture.binding.workspaceDigest, requestedModel: fixture.binding.requestedModel, effectiveModel: fixture.binding.effectiveModel, @@ -140,7 +140,7 @@ describe("ACPX recovery identity", () => { { ...fixture.expected, normalizedSessionId: otherBinding.normalizedSessionId, - profileDigest: otherBinding.profileDigest, + profileDigest: otherBinding.commandDigest, workspaceDigest: otherBinding.workspaceDigest, }, otherBinding, @@ -176,17 +176,6 @@ describe("ACPX recovery identity", () => { expect(() => verifyExpectedAcpxIdentity(fixture.expected, fixture.binding, earlyV1), ).toThrow(/persisted runtime record/); - expect(() => - verifyExpectedAcpxIdentity( - { - ...fixture.expected, - profileDigest: fixture.input.profile.commandDigest, - }, - fixture.binding, - earlyV1, - ), - ).toThrow(/immutable session configuration/); - const changedBinding = await createAcpxRecoveryBinding({ ...fixture.input, profile: { @@ -197,11 +186,12 @@ describe("ACPX recovery identity", () => { expect(changedBinding.profileDigest).not.toBe( fixture.binding.profileDigest, ); + expect(changedBinding.commandDigest).toBe(fixture.binding.commandDigest); expect(() => verifyExpectedAcpxIdentity( { ...fixture.expected, - profileDigest: changedBinding.profileDigest, + profileDigest: changedBinding.commandDigest, }, changedBinding, earlyV1, @@ -275,7 +265,7 @@ async function recoveryFixture() { acpxRecordId: "record-1", backendSessionId: "backend-1", agentSessionId: "agent-1", - profileDigest: binding.profileDigest, + profileDigest: binding.commandDigest, workspaceDigest: binding.workspaceDigest, requestedModel: binding.requestedModel, effectiveModel: binding.effectiveModel, diff --git a/packages/paperclip-runner/src/drivers/acpx/recovery-identity.ts b/packages/paperclip-runner/src/drivers/acpx/recovery-identity.ts index d48faccec3..a0b8f5249b 100644 --- a/packages/paperclip-runner/src/drivers/acpx/recovery-identity.ts +++ b/packages/paperclip-runner/src/drivers/acpx/recovery-identity.ts @@ -14,6 +14,7 @@ export interface AcpxRecoveryBinding { workspacePath: string; workspaceDigest: string; runtimeRoot: string; + commandDigest: string; profileDigest: string; requestedModel: string; effectiveModel: string; @@ -87,6 +88,7 @@ export async function createAcpxRecoveryBinding(input: { workspacePath, workspaceDigest, runtimeRoot, + commandDigest: input.profile.commandDigest, profileDigest, requestedModel: input.requestedModel, effectiveModel: input.requestedModel, @@ -117,19 +119,25 @@ export function createAcpxIdentityRecord( /** Project the private persisted record into the PRP sidecar wire identity. */ export function acpxProviderSessionIdentity( record: AcpxIdentityRecord, + binding: AcpxRecoveryBinding, ): AcpxExpectedSessionIdentity { - return { + const identity: AcpxExpectedSessionIdentity = { kind: "acpx", normalizedSessionId: record.normalizedSessionId, acpxRecordId: record.acpxRecordId, backendSessionId: record.backendSessionId, agentSessionId: record.agentSessionId, - profileDigest: record.profileDigest, + // The PRP provider contract historically names this field + // `profileDigest`, but it attests the qualified executable digest. Keep + // the broader immutable-profile digest private in the persisted record. + profileDigest: binding.commandDigest, workspaceDigest: record.workspaceDigest, requestedModel: record.requestedModel, effectiveModel: record.effectiveModel, permissionMode: record.permissionMode, }; + verifyExpectedAcpxIdentity(identity, binding, record); + return identity; } /** @@ -146,7 +154,7 @@ export function verifyExpectedAcpxIdentity( validateExpected(expected); if ( expected.normalizedSessionId !== binding.normalizedSessionId || - expected.profileDigest !== binding.profileDigest || + expected.profileDigest !== binding.commandDigest || expected.workspaceDigest !== binding.workspaceDigest || expected.requestedModel !== binding.requestedModel || expected.effectiveModel !== binding.effectiveModel || diff --git a/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts b/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts index a8fd65fe95..0345ffcf44 100644 --- a/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts +++ b/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts @@ -448,7 +448,7 @@ export class AcpxRuntimeHost { kind: "acpx", normalizedSessionId: binding.normalizedSessionId, ...runtimeIdentity, - profileDigest: binding.profileDigest, + profileDigest: binding.commandDigest, workspaceDigest: binding.workspaceDigest, requestedModel: binding.requestedModel, effectiveModel: binding.effectiveModel,