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

@@ -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;
}