From 6363949b650ca95a22aa9eff916029037455f900 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 27 May 2026 19:11:43 +0000 Subject: [PATCH] Fix Codex resume adopting stale looser approval policy When thread/resume echoed a permissive approval policy with the same sandbox as the user-selected mode, applyCodexEffectiveThreadState could overwrite stricter on-request settings with stale untrusted policy. Prefer ADE-requested policy whenever resume responses disagree. Co-authored-by: Arul Sharma --- .../services/chat/agentChatService.test.ts | 49 +++++++++++++++++++ .../main/services/chat/agentChatService.ts | 7 +-- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/main/services/chat/agentChatService.test.ts b/apps/desktop/src/main/services/chat/agentChatService.test.ts index f701e4ebf..0a3a31d20 100644 --- a/apps/desktop/src/main/services/chat/agentChatService.test.ts +++ b/apps/desktop/src/main/services/chat/agentChatService.test.ts @@ -11768,6 +11768,55 @@ describe("createAgentChatService", () => { expect(turnStartParams?.effort).toBe("medium"); }); + it("keeps stricter Codex approval policy when resume echoes stale looser policy with the same sandbox", async () => { + vi.mocked(mapPermissionToCodex).mockImplementation((mode) => { + if (mode === "default") { + return { approvalPolicy: "on-request", sandbox: "workspace-write" }; + } + if (mode === "edit") { + return { approvalPolicy: "untrusted", sandbox: "workspace-write" }; + } + return { approvalPolicy: "on-request", sandbox: "read-only" }; + }); + mockState.codexResponseOverrides.set("thread/resume", () => ({ + thread: { id: "thread-stale-untrusted" }, + approvalPolicy: "unlessTrusted", + sandbox: { type: "workspaceWrite" }, + })); + + const { service } = createService(); + const session = await service.createSession({ + laneId: "lane-1", + provider: "codex", + model: "gpt-5.4", + permissionMode: "default", + }); + + await service.dispose({ sessionId: session.id }); + writePersistedChatState(session.id, { + ...readPersistedChatState(session.id), + threadId: "thread-stale-untrusted", + codexApprovalPolicy: "on-request", + codexSandbox: "workspace-write", + codexConfigSource: "flags", + permissionMode: "default", + }); + + const resumed = await service.resumeSession({ sessionId: session.id }); + + const resumeRequest = mockState.codexRequestPayloads.find((payload) => payload.method === "thread/resume"); + const resumeParams = resumeRequest?.params as { approvalPolicy?: unknown; sandbox?: unknown } | undefined; + expect(resumeParams?.approvalPolicy).toBe("on-request"); + expect(resumeParams?.sandbox).toBe("workspace-write"); + expect(resumed.codexApprovalPolicy).toBe("on-request"); + expect(resumed.codexSandbox).toBe("workspace-write"); + expect(resumed.permissionMode).toBe("default"); + + const persistedAfter = readPersistedChatState(session.id); + expect(persistedAfter.codexApprovalPolicy).toBe("on-request"); + expect(persistedAfter.codexSandbox).toBe("workspace-write"); + }); + it("keeps Codex planner approval guard scoped to the turn that started in plan mode", async () => { vi.mocked(mapPermissionToCodex).mockImplementation((mode) => { if (mode === "full-auto") { diff --git a/apps/desktop/src/main/services/chat/agentChatService.ts b/apps/desktop/src/main/services/chat/agentChatService.ts index 7aae46338..340d85928 100644 --- a/apps/desktop/src/main/services/chat/agentChatService.ts +++ b/apps/desktop/src/main/services/chat/agentChatService.ts @@ -3471,10 +3471,11 @@ function shouldPreserveRequestedCodexPolicy( ): boolean { if (!requested) return false; // Resume responses can echo stale thread policy from before ADE re-sent the - // picker/config flags. Fresh starts still adopt the runtime-reported policy. + // picker/config flags. Fresh starts omit requestedCodexPolicy and still adopt + // the runtime-reported policy. if (runtime.sandbox && runtime.sandbox !== requested.sandbox) return true; - if (!runtime.approvalPolicy || runtime.approvalPolicy === requested.approvalPolicy) return false; - return requested.approvalPolicy === "never" || requested.approvalPolicy === "untrusted"; + if (runtime.approvalPolicy && runtime.approvalPolicy !== requested.approvalPolicy) return true; + return false; } function applyCodexEffectiveThreadState(