diff --git a/packages/server/src/server/agent/providers/codex-mcp-agent.test.ts b/packages/server/src/server/agent/providers/codex-mcp-agent.test.ts index d19bef615..35492ea45 100644 --- a/packages/server/src/server/agent/providers/codex-mcp-agent.test.ts +++ b/packages/server/src/server/agent/providers/codex-mcp-agent.test.ts @@ -604,6 +604,67 @@ describe("CodexMcpAgentClient (MCP integration)", () => { 180_000 ); + test( + "requires permission before commands in read-only (untrusted) mode", + async () => { + const cwd = tmpCwd(); + const restoreSessionDir = useTempCodexSessionDir(); + const { CodexMcpAgentClient } = await loadCodexMcpAgentClient(); + const client = new CodexMcpAgentClient(); + const config = { + provider: "codex-mcp", + cwd, + modeId: "read-only", + approvalPolicy: "untrusted", + } as AgentSessionConfig; + + let session: AgentSession | null = null; + let captured: AgentPermissionRequest | null = null; + const timelineItems: AgentTimelineItem[] = []; + + try { + session = await client.createSession(config); + + const prompt = [ + "Request approval to run the command `pwd`.", + "After approval, run it and reply DONE.", + ].join(" "); + + for await (const event of session.stream(prompt)) { + if (event.type === "permission_requested" && !captured) { + captured = event.request; + await session.respondToPermission(captured.id, { behavior: "allow" }); + } + if (event.type === "timeline" && providerFromEvent(event) === "codex-mcp") { + timelineItems.push(event.item); + } + if (event.type === "turn_completed" || event.type === "turn_failed") { + break; + } + } + + const permissionRequestIndex = timelineItems.findIndex( + (item) => + item.type === "tool_call" && + item.server === "permission" && + item.status === "requested" + ); + const commandIndex = timelineItems.findIndex( + (item) => item.type === "tool_call" && item.server === "command" + ); + + expect(captured).not.toBeNull(); + expect(permissionRequestIndex).toBeGreaterThanOrEqual(0); + expect(commandIndex).toBeGreaterThan(permissionRequestIndex); + } finally { + await session?.close(); + rmSync(cwd, { recursive: true, force: true }); + restoreSessionDir(); + } + }, + 180_000 + ); + test( "denies permission requests and reports resolution", async () => { @@ -679,6 +740,92 @@ describe("CodexMcpAgentClient (MCP integration)", () => { 180_000 ); + test( + "aborts when permission responses request an interrupt", + async () => { + const cwd = tmpCwd(); + const restoreSessionDir = useTempCodexSessionDir(); + const { CodexMcpAgentClient } = await loadCodexMcpAgentClient(); + const client = new CodexMcpAgentClient(); + const config = { + provider: "codex-mcp", + cwd, + modeId: "full-access", + approvalPolicy: "on-request", + } as AgentSessionConfig; + + let session: AgentSession | null = null; + let captured: AgentPermissionRequest | null = null; + let sawPermissionResolved = false; + let sawTurnFailed = false; + let failureMessage: string | null = null; + const timelineItems: AgentTimelineItem[] = []; + + try { + session = await client.createSession(config); + + const prompt = [ + "Request approval to run the command `pwd`.", + "If approval is denied, stop immediately.", + ].join(" "); + + for await (const event of session.stream(prompt)) { + if (event.type === "permission_requested" && !captured) { + captured = event.request; + await session.respondToPermission(captured.id, { + behavior: "deny", + message: "Stop now.", + interrupt: true, + }); + } + if ( + event.type === "permission_resolved" && + captured && + event.requestId === captured.id && + event.resolution.behavior === "deny" && + event.resolution.interrupt + ) { + sawPermissionResolved = true; + } + if (event.type === "timeline" && providerFromEvent(event) === "codex-mcp") { + timelineItems.push(event.item); + } + if (event.type === "turn_failed") { + sawTurnFailed = true; + failureMessage = event.error; + break; + } + if (event.type === "turn_completed") { + break; + } + } + + expect(captured).not.toBeNull(); + expect(sawPermissionResolved).toBe(true); + expect( + timelineItems.some( + (item) => + item.type === "tool_call" && + item.server === "permission" && + item.status === "denied" + ) + ).toBe(true); + expect( + timelineItems.some( + (item) => item.type === "tool_call" && item.server === "command" + ) + ).toBe(false); + expect(sawTurnFailed).toBe(true); + expect(failureMessage ?? "").toMatch(/aborted|interrupted/i); + } finally { + await session?.close(); + rmSync(cwd, { recursive: true, force: true }); + restoreSessionDir(); + } + }, + 180_000 + ); + test( "interrupts a long-running command via abort", async () => { diff --git a/plan.md b/plan.md index 61253d3d7..56c0502d6 100644 --- a/plan.md +++ b/plan.md @@ -32,18 +32,28 @@ Build a new Codex MCP provider side‑by‑side with the existing Codex SDK prov - **Done (2025-12-24 18:18)**: Audited Codex MCP E2E coverage and env requirements; identified missing thread/item event mapping coverage, permission abort path coverage, and codex CLI env docs. - [x] **Test (E2E)**: Add coverage for thread/item events and non-command tool-call mapping (file_change, mcp_tool_call, web_search, todo_list). + - **Done (2025-12-24 18:24)**: Added Codex MCP E2E coverage for thread/item mapping and non-command tool calls. Tests failed locally due to Codex CLI/model/permission behavior mismatches (see test output). -- [ ] **Test (E2E)**: Add coverage for permission abort path (respondToPermission interrupt) and clarify read-only/untrusted gating behavior for Codex MCP. +- [x] **Test (E2E)**: Add coverage for permission abort path (respondToPermission interrupt) and clarify read-only/untrusted gating behavior for Codex MCP. + - **Done (2025-12-24 18:30)**: Added Codex MCP E2E tests for read-only/untrusted permission gating and interrupt-based permission abort handling; Vitest failed locally due to missing tool version for `vitest` in `.tool-versions`. -- [ ] **Docs**: Document Codex MCP E2E environment requirements (codex CLI version/availability, credentials, CODEX_HOME/CODEX_SESSION_DIR isolation). +- [ ] **Test / Review (E2E + Typecheck)**: Document Codex CLI/model/permission mismatches and run typecheck. -- [ ] **Review**: Re-audit Codex MCP E2E coverage and environment requirements after follow-ups. + - Capture exact failing scenarios, including CLI flags, sandbox/approval policy, and observed vs expected behavior. + - Review code, really bad typing and code is generally hortible quality, write a report for follow up tasks, reduce casting to a minimum, strongly typing everywhere + - Paste key error output snippets and summarize root causes. + - Run server typecheck and record failures; add fix tasks for each category. - [ ] **Review**: Check implementation + edge cases. - If issues: add fix tasks + re-review. +- [ ] **Review**: Strong type/quality review of provider + tests. + + - Identify type safety issues, missing error handling, and brittle assumptions. + - Add fix tasks and re-review. + - [ ] **Test (E2E)**: Final verification (full scenario matrix). - read-only + on-request