fix(runner): preserve ACPX command attestation

This commit is contained in:
Dotta 2026-09-02 23:57:10 -05:00
parent d2c9977065
commit 5835c98675
4 changed files with 34 additions and 24 deletions

View File

@ -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),

View File

@ -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,

View File

@ -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 ||

View File

@ -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,