From ceb28bdba4ed1bf38abff1999476ad73b85a2207 Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 29 Sep 2026 16:27:03 -0500 Subject: [PATCH] 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 --- .../native-session-executor.test.ts | 53 +++++++++++++++++++ .../native-runtime/native-session-executor.ts | 20 +++++++ 2 files changed, 73 insertions(+) diff --git a/server/src/services/native-runtime/native-session-executor.test.ts b/server/src/services/native-runtime/native-session-executor.test.ts index ca537cb0fb..9cfa4544ed 100644 --- a/server/src/services/native-runtime/native-session-executor.test.ts +++ b/server/src/services/native-runtime/native-session-executor.test.ts @@ -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"], diff --git a/server/src/services/native-runtime/native-session-executor.ts b/server/src/services/native-runtime/native-session-executor.ts index aa5bba4699..e07b82088d 100644 --- a/server/src/services/native-runtime/native-session-executor.ts +++ b/server/src/services/native-runtime/native-session-executor.ts @@ -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; }