diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts index 694cb7c9c8..ab471a011a 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts @@ -744,6 +744,94 @@ describe("Codex app-server provider", () => { }); }); + test("Default Permissions pins the user as approvals reviewer on thread/start", async () => { + const requests: Array<{ method: string; params: unknown }> = []; + const session = createSession( + { modeId: "auto", thinkingOptionId: "medium" }, + { autoReviewEnabled: true }, + ); + session.currentThreadId = null; + session.activeForegroundTurnId = null; + session.client = { + request: vi.fn(async (method: string, params: unknown) => { + requests.push({ method, params }); + if (method === "thread/start") { + return { thread: { id: "default-mode-thread" } }; + } + if (method === "turn/start") { + return {}; + } + throw new Error(`Unexpected request: ${method}`); + }), + }; + + await session.startTurn("trigger thread creation"); + + const startCall = requests.find((req) => req.method === "thread/start"); + expect(startCall?.params).toMatchObject({ + approvalPolicy: "on-request", + sandbox: "workspace-write", + approvalsReviewer: "user", + }); + }); + + test("switching from auto-review back to Default returns approvals to the user", async () => { + const session = createSession({ modeId: "auto-review" }, { autoReviewEnabled: true }); + const request = vi.fn(async (method: string) => { + if (method === "thread/loaded/list") { + return { data: ["test-thread"] }; + } + if (method === "turn/start") { + return {}; + } + throw new Error(`Unexpected request: ${method}`); + }); + session.activeForegroundTurnId = null; + session.client = createStub({ request }); + + await session.setMode("auto"); + await session.startTurn("needs approval"); + + const turnStartCall = request.mock.calls.find(([method]) => method === "turn/start"); + expect(turnStartCall?.[1]).toEqual( + expect.objectContaining({ + approvalPolicy: "on-request", + approvalsReviewer: "user", + }), + ); + }); + + test("Read-only pins the user as approvals reviewer on thread/start", async () => { + const requests: Array<{ method: string; params: unknown }> = []; + const session = createSession( + { modeId: "read-only", thinkingOptionId: "medium" }, + { autoReviewEnabled: true }, + ); + session.currentThreadId = null; + session.activeForegroundTurnId = null; + session.client = { + request: vi.fn(async (method: string, params: unknown) => { + requests.push({ method, params }); + if (method === "thread/start") { + return { thread: { id: "read-only-thread" } }; + } + if (method === "turn/start") { + return {}; + } + throw new Error(`Unexpected request: ${method}`); + }), + }; + + await session.startTurn("trigger thread creation"); + + const startCall = requests.find((req) => req.method === "thread/start"); + expect(startCall?.params).toMatchObject({ + approvalPolicy: "on-request", + sandbox: "read-only", + approvalsReviewer: "user", + }); + }); + test("setMode and setThinkingOption return a next-turn notice while a turn is active", async () => { const session = createSession({ modeId: "auto", thinkingOptionId: "medium" }); @@ -834,6 +922,7 @@ describe("Codex app-server provider", () => { const turnStart = request.mock.calls.find(([method]) => method === "turn/start")?.[1]; expect(turnStart).not.toHaveProperty("approvalPolicy"); expect(turnStart).not.toHaveProperty("sandboxPolicy"); + expect(turnStart).not.toHaveProperty("approvalsReviewer"); }); test("carries the complete native workspace-write policy including writable roots", async () => { diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index 1c43612fa4..84f31c1bda 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -273,17 +273,19 @@ interface CodexAppServerAgentDeps { interface CodexModePreset { approvalPolicy: string; sandbox: string; - approvalsReviewer?: "auto_review"; + approvalsReviewer: "user" | "auto_review"; } const MODE_PRESETS: Record = { "read-only": { approvalPolicy: "on-request", sandbox: "read-only", + approvalsReviewer: "user", }, auto: { approvalPolicy: "on-request", sandbox: "workspace-write", + approvalsReviewer: "user", }, "auto-review": { approvalPolicy: "on-request", @@ -293,6 +295,7 @@ const MODE_PRESETS: Record = { "full-access": { approvalPolicy: "never", sandbox: "danger-full-access", + approvalsReviewer: "user", }, }; @@ -300,15 +303,6 @@ function isAutoReviewReviewer(value: string | undefined): boolean { return value === "auto_review" || value === "guardian_subagent"; } -function applyApprovalsReviewerParam( - params: Record, - preset: CodexModePreset, -): void { - if (preset.approvalsReviewer) { - params.approvalsReviewer = preset.approvalsReviewer; - } -} - function shouldPromoteThreadResponseToAutoReview(params: { approvalsReviewer: string | undefined; approvalPolicy: string; @@ -4151,7 +4145,7 @@ export class CodexAppServerAgentSession implements AgentSession { : toSandboxPolicy(sandboxPolicyType, workspaceWrite); } if (this.hasWorkflowModeOverride) { - applyApprovalsReviewerParam(params, preset); + params.approvalsReviewer = preset.approvalsReviewer; } return { approvalPolicy, sandboxPolicyType }; } @@ -5205,7 +5199,7 @@ export class CodexAppServerAgentSession implements AgentSession { ...(this.ephemeral ? { ephemeral: true } : {}), }; if (this.hasWorkflowModeOverride) { - applyApprovalsReviewerParam(params, preset); + params.approvalsReviewer = preset.approvalsReviewer; } return { params, approvalPolicy, sandbox }; }