fix: validate Cursor plan waits against canonical contract policy

Reuse the production completion-contract envelope hash and exercise real contract creation and reuse in accepted-plan persistence tests.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-30 02:54:16 -05:00
1 parent b74ca5eb97
commit c58a8881f2
4 files changed
+38 -15

No files matched your search

@@ -1,3 +1,4 @@
import { ensureNativeCompletionContract } from "../services/native-runtime/completion-contracts.js";
import { readNativeCursorPlanWait, hasCommittedNativeCursorPlanWait } from "../services/native-runtime/native-cursor-plan-wait.js";
import { nativeSha256 } from "../services/native-runtime/canonical.js";
import { buildQuestionResponseDeliveryEnvelope } from "../services/question-response-delivery.js";
@@ -13735,14 +13736,17 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
async function seedAcceptedCursorPlanWait(semanticFinish = false, sourceSequenceOffset = 0) {
const f = await seedStrandedIssueFixture({ status: "in_progress", runStatus: "succeeded", livenessState: "advanced" });
const contractId = randomUUID(), instance = randomUUID(), interactionId = randomUUID();
const contract = { revision: "1", objective: "Review the plan before further work", criteria: [{ id: "objective", requirement: "Explicit completion required" }] };
const instance = randomUUID(), interactionId = randomUUID();
const contractInput = { db, companyId: f.companyId,
issue: { id: f.issueId, title: "Review the plan before further work", description: "Explicit completion required" }, actorId: "test" };
const { row: persistedContract, contract } = await ensureNativeCompletionContract(contractInput);
const reused = await ensureNativeCompletionContract(contractInput);
expect(reused.row.id).toBe(persistedContract.id);
expect(reused.contract).toEqual(contract);
const contractId = persistedContract.id, contractSha = persistedContract.canonicalSha256;
// Production binds policy/schema as well as the body; a body-only hash is not a valid contract receipt.
expect(contractSha).not.toBe(nativeSha256(contract));
const model = "gpt-5.6-luna[context=272k,reasoning=medium,fast=false]";
const contractSha = nativeSha256(contract);
await db.insert(completionContracts).values({ id: contractId, companyId: f.companyId, issueId: f.issueId,
revision: 1, schemaVersion: "paperclip.completion-contract.v1", policyVersion: "phase6-v7", risk: "low",
completionAuthority: "agent_claim_policy", incompleteCriteriaPolicy: "preserve_non_terminal", contractJson: contract,
canonicalSha256: contractSha, createdByActorType: "system", createdByActorId: "test" });
await db.update(agents).set({ adapterType: "paperclip_runner", adapterConfig: { provider: "acpx", acpxAgent: "cursor", model, acpxSessionMode: "plan", acpxPermissionMode: "approve-all" } }).where(eq(agents.id, f.agentId));
await db.update(agentWakeupRequests).set({ status: "completed" }).where(eq(agentWakeupRequests.id, f.wakeupRequestId));
await db.update(heartbeatRuns).set({ runtimeMode: "native", nativeIssueId: f.issueId, nativeSessionId: f.runId,
@@ -10,6 +10,18 @@ import { nativeSha256 } from "./canonical.js";
export const NATIVE_COMPLETION_CONTRACT_SCHEMA = "paperclip.completion-contract.v1";
export const NATIVE_COMPLETION_POLICY_VERSION = "phase6-v4";
/** Canonical persisted receipt includes the policy that governs the contract. */
export function nativeCompletionContractSha256(row: Pick<typeof completionContracts.$inferSelect,
"schemaVersion" | "policyVersion" | "risk" | "completionAuthority" | "contractJson">): string {
return nativeSha256({
schemaVersion: row.schemaVersion,
policyVersion: row.policyVersion,
risk: row.risk,
completionAuthority: row.completionAuthority,
contract: row.contractJson,
});
}
type CompletionComment = {
id?: string;
body: string;
@@ -162,11 +174,11 @@ export async function ensureNativeCompletionContract(input: {
immediateRequests: input.immediateRequests,
humanResponseId: input.humanResponseId,
});
const latestCandidateSha256 = nativeSha256({
const latestCandidateSha256 = nativeCompletionContractSha256({
schemaVersion: NATIVE_COMPLETION_CONTRACT_SCHEMA,
policyVersion: NATIVE_COMPLETION_POLICY_VERSION,
...policy,
contract: latestCandidate,
contractJson: latestCandidate as unknown as Record<string, unknown>,
});
if (latest?.canonicalSha256 === latestCandidateSha256) {
return { row: latest, contract: latestCandidate, sources: buildNativeCompletionContractSources(input) };
@@ -179,11 +191,11 @@ export async function ensureNativeCompletionContract(input: {
immediateRequests: input.immediateRequests,
humanResponseId: input.humanResponseId,
});
const canonicalSha256 = nativeSha256({
const canonicalSha256 = nativeCompletionContractSha256({
schemaVersion: NATIVE_COMPLETION_CONTRACT_SCHEMA,
policyVersion: NATIVE_COMPLETION_POLICY_VERSION,
...policy,
contract,
contractJson: contract as unknown as Record<string, unknown>,
});
const [row] = await tx.insert(completionContracts).values({
companyId: input.companyId,
@@ -2,12 +2,15 @@ import { describe, expect, it, vi } from "vitest";
import * as runner from "../../vendor/paperclip-runner/index.js";
import { resolveQualifiedAcpxProfile, validatePrpStructuredRunResult } from "../../vendor/paperclip-runner/index.js";
import { buildQuestionResponseDeliveryEnvelope } from "../question-response-delivery.js";
import { NATIVE_COMPLETION_CONTRACT_SCHEMA, NATIVE_COMPLETION_POLICY_VERSION } from "./completion-contracts.js";
import { nativeSha256 } from "./canonical.js";
import { nativeCursorPlanWaitFromFacts, type CursorPlanWaitFacts } from "./native-cursor-plan-wait.js";
function fixture(): CursorPlanWaitFacts {
const b = { companyId: "company", issueId: "issue", agentId: "agent", runId: "run" };
const contract = { revision: "revision", criteria: [{ id: "criterion" }] };
const contractMetadata = { schemaVersion: NATIVE_COMPLETION_CONTRACT_SCHEMA, policyVersion: NATIVE_COMPLETION_POLICY_VERSION, risk: "low", completionAuthority: "agent_claim_policy" };
const contractSha = nativeSha256({ ...contractMetadata, contract });
const planId = `plan-${"a".repeat(64)}`;
const input = { schema: "paperclip.question_set.v1", title: "Plan", description: "Exact revised plan text", questions: [{ id: planId, prompt: "Proceed?", required: true, answerMode: "single_select", options: [{ id: "accept", label: "Accept" }, { id: "reject", label: "Reject" }, { id: "cancel", label: "Cancel" }] }] };
const i = { id: "interaction", ...b, sourceRunId: b.runId, createdByAgentId: b.agentId, resolvedByUserId: "board", resolvedByAgentId: null, resolvedAt: new Date(0),
@@ -21,8 +24,8 @@ function fixture(): CursorPlanWaitFacts {
};
return {
binding: b,
run: { id: b.runId, companyId: b.companyId, agentId: b.agentId, nativeIssueId: b.issueId, runtimeMode: "native", runnerInstanceId: "instance", status: "running", completionContractId: "contract", completionContractSha256: nativeSha256(contract), runnerProfileJson: { nativeExecutionInput: { binding: b, provider: { kind: "acpx", agent: "cursor", cursorMode: "plan", model: "gpt-5.6-luna[context=272k,reasoning=medium,fast=false]", profile: resolveQualifiedAcpxProfile("cursor", "gpt-5.6-luna[context=272k,reasoning=medium,fast=false]") }, session: { normalizedSessionId: "session" }, completionContract: { id: "contract", sha256: nativeSha256(contract), contract } } } },
contract: { id: "contract", canonicalSha256: nativeSha256(contract), contractJson: contract },
run: { id: b.runId, companyId: b.companyId, agentId: b.agentId, nativeIssueId: b.issueId, runtimeMode: "native", runnerInstanceId: "instance", status: "running", completionContractId: "contract", completionContractSha256: contractSha, runnerProfileJson: { nativeExecutionInput: { binding: b, provider: { kind: "acpx", agent: "cursor", cursorMode: "plan", model: "gpt-5.6-luna[context=272k,reasoning=medium,fast=false]", profile: resolveQualifiedAcpxProfile("cursor", "gpt-5.6-luna[context=272k,reasoning=medium,fast=false]") }, session: { normalizedSessionId: "session" }, completionContract: { id: "contract", sha256: contractSha, contract } } } },
contract: { id: "contract", ...contractMetadata, canonicalSha256: contractSha, contractJson: contract },
events: [
event(275, "runtime_request.created", { request: { schema: "paperclip.runtime_request.v2", status: "pending", type: "input", requestKind: "runtime", requestId: "request", turnId: "turn", itemId: "native-plan-tool", origin: { provider: "cursor", method: "cursor/create_plan", adapter: "acpx-runtime-sidecar" }, input } }),
event(294, "tool.execution.started", { schema: "paperclip.tool.execution.v1", executionId: "native-plan-tool", transport: "builtin", operation: "execute", status: "running", name: "arbitrary display name" }),
@@ -110,6 +113,9 @@ describe("accepted Cursor plan passive-wait authority", () => {
["cancelled run", (f: CursorPlanWaitFacts) => { f.run.status = "cancelled"; }],
["Agent mode", (f: CursorPlanWaitFacts) => { (f.run.runnerProfileJson as any).nativeExecutionInput.provider.cursorMode = "agent"; }],
["stale profile", (f: CursorPlanWaitFacts) => { (f.run.runnerProfileJson as any).nativeExecutionInput.provider.profile = { ...resolveQualifiedAcpxProfile("cursor", "gpt-5.6-luna[context=272k,reasoning=medium,fast=false]"), commandDigest: "old" }; }],
["changed contract policy", (f: CursorPlanWaitFacts) => { f.contract.policyVersion = "tampered-policy"; }],
["changed completion authority", (f: CursorPlanWaitFacts) => { f.contract.completionAuthority = "server_arbiter"; }],
["changed contract hash", (f: CursorPlanWaitFacts) => { f.contract.canonicalSha256 = "f".repeat(64); }],
["changed contract", (f: CursorPlanWaitFacts) => { f.contract.contractJson.revision = "new"; }],
["unknown origin", (f: CursorPlanWaitFacts) => editEvent(f, 0, e => { e.payload.request.origin.provider = "acpx"; })],
["wrong method", (f: CursorPlanWaitFacts) => editEvent(f, 0, e => { e.payload.request.origin.method = "cursor/ask_question"; })],
@@ -10,6 +10,7 @@ import {
type PrpStructuredRunResult,
} from "../../vendor/paperclip-runner/index.js";
import { buildQuestionResponseDeliveryEnvelope } from "../question-response-delivery.js";
import { nativeCompletionContractSha256 } from "./completion-contracts.js";
import { nativeSha256 } from "./canonical.js";
type Binding = { companyId: string; issueId: string; runId: string; agentId: string };
@@ -43,7 +44,7 @@ export interface NativeCursorPlanWaitSource extends Binding {
export interface CursorPlanWaitFacts {
binding: Binding;
run: Pick<typeof heartbeatRuns.$inferSelect, "id" | "companyId" | "agentId" | "nativeIssueId" | "runtimeMode" | "status" | "runnerProfileJson" | "runnerInstanceId" | "completionContractId" | "completionContractSha256">;
contract: Pick<typeof completionContracts.$inferSelect, "id" | "canonicalSha256" | "contractJson">;
contract: Pick<typeof completionContracts.$inferSelect, "id" | "canonicalSha256" | "contractJson" | "schemaVersion" | "policyVersion" | "risk" | "completionAuthority">;
events: Array<Pick<typeof heartbeatRunEvents.$inferSelect, "companyId" | "agentId" | "runId" | "seq" | "eventType" | "payload" | "sourceInstanceId" | "sourceEventId" | "sourceSeq" | "sourcePayloadSha256" | "protocolSchemaVersion">>;
interactions: Array<{ interaction: typeof issueThreadInteractions.$inferSelect; delivery: typeof issueQuestionResponseDeliveries.$inferSelect }>;
}
@@ -71,7 +72,7 @@ function cursorPlanWaitFromFacts(facts: CursorPlanWaitFacts, committedSource?: N
}
if (run.id !== b.runId || run.companyId !== b.companyId || run.agentId !== b.agentId || run.nativeIssueId !== b.issueId || run.runtimeMode !== "native" || !["running", "succeeded"].includes(run.status) ||
!Object.entries(b).every(([key, value]) => record(admission.binding)[key] === value) || provider.kind !== "acpx" || provider.agent !== "cursor" || provider.cursorMode !== "plan" || typeof provider.model !== "string" || !provider.model.trim() ||
record(admission.completionContract).id !== contract.id || record(admission.completionContract).sha256 !== contract.canonicalSha256 || nativeSha256(contract.contractJson) !== contract.canonicalSha256 || run.completionContractId !== contract.id || run.completionContractSha256 !== contract.canonicalSha256 || !same(contract.contractJson, record(admission.completionContract).contract)) return null;
record(admission.completionContract).id !== contract.id || record(admission.completionContract).sha256 !== contract.canonicalSha256 || nativeCompletionContractSha256(contract) !== contract.canonicalSha256 || run.completionContractId !== contract.id || run.completionContractSha256 !== contract.canonicalSha256 || !same(contract.contractJson, record(admission.completionContract).contract)) return null;
const sessionId = record(admission.session).normalizedSessionId;
if (typeof sessionId !== "string" || !sessionId || facts.events.length === 0 || facts.events.length > 1000) return null;
const events: Array<Record<string, any>> = [];