Use provider-neutral modes in Runner contracts

Carry opaque mode identifiers through native execution, ACPX sidecar and Rust recovery. Delegate supported values and acknowledgement to provider adapters. Preserve exact historical plan waits and fence incompatible pre-release sessions.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-05 21:26:52 -05:00
1 parent e9e8c98ecd
commit 56ef7c2b2c
40 files changed
+444 -246

No files matched your search

@@ -4,7 +4,7 @@ import { resolveQualifiedAcpxProfile, validatePrpStructuredRunResult } from "../
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";
import { hasCommittedNativeCursorPlanWait, nativeCursorPlanWaitFromFacts, type CursorPlanWaitFacts } from "./native-cursor-plan-wait.js";
function fixture(): CursorPlanWaitFacts {
const b = { companyId: "company", issueId: "issue", agentId: "agent", runId: "run" };
@@ -24,7 +24,7 @@ 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: 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 } } } },
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", mode: "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 } }),
@@ -100,6 +100,45 @@ describe("accepted Cursor plan passive-wait authority", () => {
f.events.unshift(structuredClone(turnStart)); resequence(f);
expect(nativeCursorPlanWaitFromFacts(f)).toBeNull();
});
it("preserves an exact committed wait using the pre-release field without admitting new legacy waits", async () => {
const f = fixture();
f.run.status = "succeeded";
const proof = nativeCursorPlanWaitFromFacts(f)!;
const admission = (f.run.runnerProfileJson as any).nativeExecutionInput;
admission.provider.cursorMode = admission.provider.mode;
delete admission.provider.mode;
expect(nativeCursorPlanWaitFromFacts(f)).toBeNull();
const payload = (type: string) => (f.events.find(e => e.eventType === type)!.payload as any).prpEvent;
const { interaction, delivery } = f.interactions[0]!;
const toolBinding = { toolExecutionId: proof.source.toolExecutionId, toolLifecycleSha256: proof.source.toolLifecycleSha256 };
const source = { ...proof.source, authoritySha256: nativeSha256({ admission, contract: f.contract,
created: payload("runtime_request.created"), resolved: payload("runtime_request.resolved"), terminal: payload("turn.completed"),
interaction, delivery, resolvedAt: interaction.resolvedAt!.toISOString(), acknowledgedAt: delivery.acknowledgedAt!.toISOString(), toolBinding }) };
const resultJson = { result: proof.result, terminal: { schema: "paperclip.prp.terminal.v1", runTerminalState: "succeeded", turnTerminalState: "completed", reportedWorkDisposition: "yielded" } };
const canonicalSha256 = nativeSha256(resultJson);
const db = () => {
const rows = [
[{ decision: { decisionJson: { cursorPlanWait: source, cursorPlanWaitResult: { resultId: "result", resultSha256: canonicalSha256 } } },
result: { id: "result", canonicalSha256, completionContractId: source.contractId, turnId: source.turnId, resultJson } }],
[{ run: f.run, contract: f.contract }], f.events, f.interactions,
[{ ...f.run, createdAt: new Date(0) }], [interaction], [], [],
];
return { select: () => {
const result = rows.shift();
const query: any = { from: () => query, innerJoin: () => query, where: () => query, orderBy: () => query, limit: () => Promise.resolve(result) };
return query;
} } as never;
};
expect(await hasCommittedNativeCursorPlanWait(db(), f.binding)).toBe(true);
// History is immutable: neither changing the old mode nor rewriting its
// field name may preserve the original acceptance authority.
admission.provider.cursorMode = "agent";
expect(await hasCommittedNativeCursorPlanWait(db(), f.binding)).toBe(false);
delete admission.provider.cursorMode;
admission.provider.mode = "plan";
expect(await hasCommittedNativeCursorPlanWait(db(), f.binding)).toBe(false);
});
it("records the accepted revision and explicitly unfinished Plan-mode continuation", () => {
const value = nativeCursorPlanWaitFromFacts(fixture());
expect(value?.source).toMatchObject({ requestId: "request", planRevision: `plan-${"a".repeat(64)}`, terminalEventId: "instance:304", toolExecutionId: "native-plan-tool" });
@@ -179,7 +218,7 @@ describe("accepted Cursor plan passive-wait authority", () => {
["wrong task", (f: CursorPlanWaitFacts) => { f.run.nativeIssueId = "other"; }],
["failed run", (f: CursorPlanWaitFacts) => { f.run.status = "failed"; }],
["cancelled run", (f: CursorPlanWaitFacts) => { f.run.status = "cancelled"; }],
["Agent mode", (f: CursorPlanWaitFacts) => { (f.run.runnerProfileJson as any).nativeExecutionInput.provider.cursorMode = "agent"; }],
["Agent mode", (f: CursorPlanWaitFacts) => { (f.run.runnerProfileJson as any).nativeExecutionInput.provider.mode = "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"; }],
@@ -57,6 +57,13 @@ function isHistoricalUnboundWait(source?: NativeCursorPlanWaitSource): boolean {
return source !== undefined && source.toolExecutionId === undefined && source.toolLifecycleSha256 === undefined;
}
// Pre-release records used a vendor-specific field. Only a previously committed
// wait may read it; the unmodified admission remains bound to its original hash.
function isPlanMode(provider: Record<string, any>, committed: boolean): boolean {
if (provider.mode !== undefined) return provider.mode === "plan" && provider.cursorMode === undefined;
return committed && provider.cursorMode === "plan";
}
/** Only committed native request/answer/normal-terminal facts can create this passive wait. */
export function nativeCursorPlanWaitFromFacts(facts: CursorPlanWaitFacts) {
return cursorPlanWaitFromFacts(facts);
@@ -75,7 +82,7 @@ function cursorPlanWaitFromFacts(facts: CursorPlanWaitFacts, committedSource?: N
if (!["agent", "driverKind", "protocolVersion", "acpxVersion", "commandDigest", "agentProfileVersion", "agentServerPackage", "agentServerVersion", "agentRuntimePackage", "agentRuntimeVersion"].every(key => profile[key] === record(expected)[key])) return null;
}
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() ||
!Object.entries(b).every(([key, value]) => record(admission.binding)[key] === value) || provider.kind !== "acpx" || provider.agent !== "cursor" || !isPlanMode(provider, committedSource !== undefined) || typeof provider.model !== "string" || !provider.model.trim() ||
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 > MAX_CONTROL_EVENTS + MAX_PROGRESS_EVENTS) return null;
@@ -187,7 +194,9 @@ async function readCursorPlanWaitProof(db: Db, binding: Binding, locked: boolean
.innerJoin(issues, and(eq(issues.id, binding.issueId), eq(issues.companyId, binding.companyId), eq(issues.assigneeAgentId, binding.agentId)))
.where(and(eq(heartbeatRuns.id, binding.runId), eq(heartbeatRuns.companyId, binding.companyId), eq(heartbeatRuns.agentId, binding.agentId), eq(heartbeatRuns.nativeIssueId, binding.issueId))).limit(1);
const [row] = await (locked ? q.for("share", { noWait: true }) : q);
if (!row || record(record(row.run.runnerProfileJson).nativeExecutionInput).provider?.agent !== "cursor" || record(record(row.run.runnerProfileJson).nativeExecutionInput).provider?.cursorMode !== "plan") return null;
if (!row) return null;
const provider = record(record(record(row.run.runnerProfileJson).nativeExecutionInput).provider);
if (provider.agent !== "cursor" || !isPlanMode(provider, committedSource !== undefined)) return null;
const eventTypes = isHistoricalUnboundWait(committedSource) ? LEGACY_EVENT_TYPES : EVENT_TYPES;
const eventLimit = MAX_CONTROL_EVENTS + (isHistoricalUnboundWait(committedSource) ? 0 : MAX_PROGRESS_EVENTS) + 1;
const eqs = db.select().from(heartbeatRunEvents).where(and(eq(heartbeatRunEvents.companyId, binding.companyId), eq(heartbeatRunEvents.runId, binding.runId), inArray(heartbeatRunEvents.eventType, eventTypes))).orderBy(asc(heartbeatRunEvents.seq)).limit(eventLimit);
@@ -104,7 +104,7 @@ export function buildNativeExecutionInput(input: {
if (input.issue.workMode !== "standard" && input.issue.workMode !== "planning" && input.issue.workMode !== "ask") {
throw new Error("native_execution_input_invalid: issue work mode must be standard, planning, or ask");
}
const cursorMode = resolvePaperclipRunnerCursorMode(input.provider, input.acpxAgent, input.acpxSessionMode);
const mode = resolvePaperclipRunnerCursorMode(input.provider, input.acpxAgent, input.acpxSessionMode);
const executionMode = input.executionMode
?? (input.issue.workMode === "planning" ? "plan" : "default");
const acpxProfile = input.provider === "acpx"
@@ -266,7 +266,7 @@ export function buildNativeExecutionInput(input: {
agent: acpxProfile!.agent,
model: input.model,
permissionMode: input.acpxPermissionMode ?? "approve-all",
...(cursorMode === undefined ? {} : { cursorMode }),
...(mode === undefined ? {} : { mode }),
profile: {
driverKind: acpxProfile!.driverKind,
protocolVersion: acpxProfile!.protocolVersion,
@@ -1745,30 +1745,30 @@ describe("split durable provider checkpoint identity", () => {
},
}) as unknown as NativeExecutionInputV1;
it.each(["agent", "plan", "ask"] as const)("requires both persisted Cursor mode bindings for %s recovery", mode => {
it.each(["agent", "plan", "ask", "architect"] as const)("requires both persisted provider mode bindings for %s recovery", mode => {
const profileDigest = `sha256:${"a".repeat(64)}`;
const input = execution({ kind: "acpx", agent: "cursor", model: "explicit-model", permissionMode: "approve-all", cursorMode: mode }, "acpx_runtime");
const input = execution({ kind: "acpx", agent: "cursor", model: "explicit-model", permissionMode: "approve-all", mode: mode }, "acpx_runtime");
const identity = {
kind: "acpx", normalizedSessionId: "native-session", acpxRecordId: "record",
backendSessionId: "backend", agentSessionId: "agent-session", profileDigest,
workspaceDigest: `sha256:${"b".repeat(64)}`, requestedModel: "explicit-model",
effectiveModel: "explicit-model", permissionMode: "approve-all", cursorMode: mode,
effectiveModel: "explicit-model", permissionMode: "approve-all", mode: mode,
providerLifetimeFenceCandidates: [53001, 53002, 53003],
};
const state = {
schema: "paperclip.runner.acpx-provider-state.v3", lifecycle: "suspended",
activeTurnId: null, providerExitUnconfirmed: false,
descriptor: { kind: "acpx", provider: "acpx", driver: "acpx_runtime", agent: "cursor",
model: "explicit-model", commandDigest: profileDigest, normalizedSessionId: "native-session", cursorMode: mode },
model: "explicit-model", commandDigest: profileDigest, normalizedSessionId: "native-session", mode: mode },
identity,
};
expect(providerSessionIdentityFromDurableProviderState({ execution: input, providerState: state })).toMatchObject({ providerSessionIdentity: identity });
for (const field of ["descriptor", "identity"] as const) {
for (const wrong of [undefined, mode === "agent" ? "plan" : "agent"]) {
const changed = structuredClone(state);
(changed[field] as Record<string, unknown>).cursorMode = wrong;
(changed[field] as Record<string, unknown>).mode = wrong;
expect(providerSessionIdentityFromDurableProviderState({ execution: input, providerState: changed })).toEqual({ providerSessionId: null, providerBackendSessionId: null, providerSessionIdentity: null });
expect(durableProviderCheckpointFailureReason(input, changed)).toBe("cursor_mode_binding");
expect(durableProviderCheckpointFailureReason(input, changed)).toBe("mode_binding");
}
}
const unsettled = { ...state, providerExitUnconfirmed: true, privateProviderData: "secret-canary" };
@@ -1,3 +1,4 @@
import { isProviderMode } from "../../vendor/paperclip-runner/index.js";
import { bundledRemoteProviderPackManifestPath, bundledRemoteRunnerBinary } from "../../vendor/paperclip-runner/index.js";
import { nativeRetryCancellationEligible, rethrowNativeCancellationLockConflict, assertCancellationRequest, cancellationIntentId as callerCancellationIntentId, cancellationRequestId } from "./native-cancellation-request.js";
import { readNativeCursorPlanWait } from "./native-cursor-plan-wait.js";
@@ -5496,7 +5497,7 @@ export function providerSessionIdentityFromDurableProviderState(input: {
identity.requestedModel !== expectedModel ||
identity.effectiveModel !== expectedModel ||
identity.permissionMode !== input.execution.provider.permissionMode ||
!acpxRecoveryCursorModeMatches(input.execution.provider, descriptor.cursorMode, identity.cursorMode) ||
!acpxRecoveryModeMatches(input.execution.provider, descriptor.mode, identity.mode) ||
!["approve-all", "approve-paperclip", "approve-reads", "deny-all"].includes(
String(identity.permissionMode),
) ||
@@ -5589,22 +5590,19 @@ export function durableProviderCheckpointFailureReason(execution: NativeExecutio
if (state.lifecycle !== "suspended") return "provider_not_suspended";
if (state.providerExitUnconfirmed !== false) return "provider_exit_unconfirmed";
if (state.activeTurnId !== null) return "provider_turn_unsettled";
if (!acpxRecoveryCursorModeMatches(execution.provider, record(state.descriptor).cursorMode, record(state.identity).cursorMode)) return "cursor_mode_binding";
if (!acpxRecoveryModeMatches(execution.provider, record(state.descriptor).mode, record(state.identity).mode)) return "mode_binding";
return "identity_binding";
}
// Recovery consumes observed identities; it must never apply the fresh-config
// default to a missing persisted mode or allow another provider to carry it.
function acpxRecoveryCursorModeMatches(
// default to a missing persisted mode. Provider capability checks belong to admission.
function acpxRecoveryModeMatches(
provider: NativeExecutionInput["provider"],
...observedModes: unknown[]
): boolean {
const expected = record(provider).cursorMode;
if (provider.kind === "acpx" && provider.agent === "cursor") {
return (expected === "agent" || expected === "plan" || expected === "ask")
&& observedModes.every(mode => mode === expected);
}
return expected === undefined && observedModes.every(mode => mode === undefined);
const expected = record(provider).mode;
return (expected === undefined || isProviderMode(expected))
&& observedModes.every(mode => mode === expected);
}
export function providerSessionIdentityTransitionIsAllowed(input: {
@@ -5612,10 +5610,10 @@ export function providerSessionIdentityTransitionIsAllowed(input: {
previous: unknown;
current: unknown;
}): boolean {
if (input.execution.provider.kind === "acpx" && !acpxRecoveryCursorModeMatches(
if (input.execution.provider.kind === "acpx" && !acpxRecoveryModeMatches(
input.execution.provider,
record(record(input.previous).providerSessionIdentity).cursorMode,
record(record(input.current).providerSessionIdentity).cursorMode,
record(record(input.previous).providerSessionIdentity).mode,
record(record(input.current).providerSessionIdentity).mode,
)) return false;
if (canonicalJson(input.previous) === canonicalJson(input.current)) {
return true;
@@ -8440,7 +8438,7 @@ async function executePaperclipNativeSessionWithinScope(
if (terminalEvent.eventType !== "turn.completed") return null;
const governedWait = await resolvePendingGovernedWait();
if (governedWait) return governedWait;
if (input.execution.provider.kind === "acpx" && input.execution.provider.agent === "cursor" && input.execution.provider.cursorMode === "plan") {
if (input.execution.provider.kind === "acpx" && input.execution.provider.agent === "cursor" && input.execution.provider.mode === "plan") {
const planWait = await readNativeCursorPlanWait(input.db, input.execution.binding);
if (planWait?.source.terminalEventId === terminalEvent.sourceEventId) return planWait.result;
}
@@ -12589,7 +12587,7 @@ async function createRunnerdBackendWithinSessionClaim(
// Read only the server operator environment, never agent/runtime env.
acpxCandidateProfile: resolveAcpxQualification(input.execution.provider, process.env),
acpxPermissionMode: input.execution.provider.permissionMode,
acpxCursorMode: input.execution.provider.cursorMode,
acpxMode: input.execution.provider.mode,
acpxPermissionModePinned:
input.execution.schema === "paperclip.native-execution-input.v4" ||
input.execution.schema === "paperclip.native-execution-input.v5",
+1
View File
@@ -117,6 +117,7 @@ export const parseCodexTurnDiff = runner.parseCodexTurnDiff;
export const parseHarnessRuntimeRequestResolution =
runner.parseHarnessRuntimeRequestResolution;
export const parseNativeExecutionInput = runner.parseNativeExecutionInput;
export const isProviderMode = runner.isProviderMode;
export const parseNativeRuntimeContext = runner.parseNativeRuntimeContext;
export const parsePaperclipQuestionSet = runner.parsePaperclipQuestionSet;
export const parsePaperclipQuestionResponse =