fix(server): bind Cursor mode at recovery identity boundaries

Reject missing, foreign, or changed observed modes before accepting suspended checkpoints or interaction-driven identity transitions.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-29 16:27:18 -05:00
1 parent c867cff960
commit ceb28bdba4
2 files changed
+73

No files matched your search

@@ -1436,6 +1436,37 @@ describe("verified native harness backups", () => {
).toBe(false);
});
it("binds Cursor mode across governed and identical recovery identities", () => {
const execution = {
...backupExecution,
provider: { kind: "acpx", agent: "cursor", model: "explicit-model", cursorMode: "plan" },
interactionResponses: [{ interactionId: "interaction-1" }],
} as unknown as NativeExecutionInputV1;
const identity = (suffix: string, cursorMode: unknown) => {
const value = acpxIdentity(suffix);
return { ...value, providerSessionIdentity: { ...value.providerSessionIdentity, cursorMode } };
};
const previous = identity("previous", "plan");
const current = identity("current", "plan");
expect(providerSessionIdentityTransitionIsAllowed({ execution, previous, current })).toBe(true);
expect(providerSessionIdentityTransitionIsAllowed({ execution, previous, current: previous })).toBe(true);
for (const mode of [undefined, null, "agent", "ask", "autopilot"]) {
const wrong = identity("wrong", mode);
expect(providerSessionIdentityTransitionIsAllowed({ execution, previous, current: wrong })).toBe(false);
expect(providerSessionIdentityTransitionIsAllowed({ execution, previous: wrong, current })).toBe(false);
expect(providerSessionIdentityTransitionIsAllowed({ execution, previous: wrong, current: wrong })).toBe(false);
}
for (const provider of [
{ kind: "acpx", agent: "cursor", model: "explicit-model" },
{ kind: "acpx", agent: "copilot", model: "explicit-model" },
{ kind: "acpx", agent: "copilot", model: "explicit-model", cursorMode: "plan" },
]) {
expect(providerSessionIdentityTransitionIsAllowed({
execution: { ...execution, provider } as unknown as NativeExecutionInputV1, previous, current,
})).toBe(false);
}
});
it("restores a verified continuation into an intentionally fresh non-reusable sandbox", () => {
expect(
shouldRestoreNativeHarnessBackupIntoSandbox({
@@ -1767,6 +1798,28 @@ describe("split durable provider checkpoint identity", () => {
});
});
it("requires the exact observed Cursor mode in suspended descriptor and identity", () => {
const profileDigest = `sha256:${"a".repeat(64)}`;
const input = execution({ kind: "acpx", agent: "cursor", model: "explicit-model", permissionMode: "deny-all", cursorMode: "plan" }, "acpx_runtime");
const providerState = {
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: "plan" },
identity: { kind: "acpx", normalizedSessionId: "native-session", acpxRecordId: "record-1", backendSessionId: "backend-1", agentSessionId: "agent-1", profileDigest,
workspaceDigest: `sha256:${"b".repeat(64)}`, requestedModel: "explicit-model", effectiveModel: "explicit-model", permissionMode: "deny-all", cursorMode: "plan", providerLifetimeFenceCandidates: [53001, 53002, 53003] },
};
const read = (state: unknown, candidate = input) => providerSessionIdentityFromDurableProviderState({ execution: candidate, providerState: state });
expect(read(providerState).providerSessionIdentity).toEqual(providerState.identity);
for (const cursorMode of [undefined, null, "agent", "ask", "autopilot"]) {
expect(read({ ...providerState, identity: { ...providerState.identity, cursorMode } }).providerSessionIdentity).toBeNull();
expect(read({ ...providerState, descriptor: { ...providerState.descriptor, cursorMode } }).providerSessionIdentity).toBeNull();
expect(read(providerState, execution({ ...input.provider, cursorMode }, "acpx_runtime")).providerSessionIdentity).toBeNull();
}
const foreign = { ...providerState, descriptor: { ...providerState.descriptor, agent: "copilot" } };
for (const cursorMode of [undefined, "plan"]) {
expect(read(foreign, execution({ ...input.provider, agent: "copilot", cursorMode }, "acpx_runtime")).providerSessionIdentity).toBeNull();
}
});
it.each([
["codex", "codex_app_server"],
["opencode", "opencode_server"],
@@ -5429,6 +5429,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) ||
!["approve-all", "approve-paperclip", "approve-reads", "deny-all"].includes(
String(identity.permissionMode),
) ||
@@ -5513,11 +5514,30 @@ function providerSessionIdentityIsPresent(value: unknown): boolean {
);
}
// 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(
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);
}
export function providerSessionIdentityTransitionIsAllowed(input: {
execution: NativeExecutionInput;
previous: unknown;
current: unknown;
}): boolean {
if (input.execution.provider.kind === "acpx" && !acpxRecoveryCursorModeMatches(
input.execution.provider,
record(record(input.previous).providerSessionIdentity).cursorMode,
record(record(input.current).providerSessionIdentity).cursorMode,
)) return false;
if (canonicalJson(input.previous) === canonicalJson(input.current)) {
return true;
}