mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-29 12:45:55 +01:00
fix(runner): preserve ACPX command attestation
This commit is contained in:
1 parent
1f1103c139
commit
5c1b7918ca
4 files changed
+34
-24
No files matched your search
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -458,7 +458,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,
|
||||
|
||||
Reference in new issue
Block a user