diff --git a/server/drivers/codex.test.ts b/server/drivers/codex.test.ts index b4407827b..3a7ad8d04 100644 --- a/server/drivers/codex.test.ts +++ b/server/drivers/codex.test.ts @@ -349,6 +349,24 @@ describe("CodexDriver turns (fake app-server)", () => { expect(JSON.parse(readFileSync(dump, "utf8")).decision).toEqual({ decision: "approved" }); }); + it("answers Codex 0.149 MCP elicitation with the MCP result shape", async () => { + await create({ mode: "mcp-elicitation" }); + const dump = join(scratch, "mcp-elicitation.json"); + process.env.FAKE_CODEX_DUMP = dump; + + await instance.adapter.sendTurn({ threadId: "t-mcp-elicitation", text: "list bots" }); + const opened = await recorder.until((e) => e.type === "request.opened"); + expect(opened).toMatchObject({ + requestType: "permission", + tool: "list_bots", + summary: 'Allow the agents MCP server to run tool "list_bots"?', + }); + + await instance.adapter.respondToRequest("t-mcp-elicitation", opened.requestId!, { behavior: "allow" }); + await recorder.until((e) => e.type === "turn.completed"); + expect(JSON.parse(readFileSync(dump, "utf8")).decision).toEqual({ action: "accept", content: {} }); + }); + it("stamps approvalScope on cards only when the turn controls this Mac", async () => { await create({ mode: "approval" }); diff --git a/server/drivers/codex.ts b/server/drivers/codex.ts index 7be92282a..464c9338f 100644 --- a/server/drivers/codex.ts +++ b/server/drivers/codex.ts @@ -249,19 +249,35 @@ export const CodexDriver: ProviderDriver = { const method = msg.method as string; const params = msg.params ?? {}; const legacy = method === "execCommandApproval" || method === "applyPatchApproval"; + const isMcpElicitation = + method === "mcpServer/elicitation/request" && + params?._meta?.codex_approval_kind === "mcp_tool_call"; const isQuestion = method === "item/tool/requestUserInput"; + const mcpTool = isMcpElicitation + ? String(params.message ?? "").match(/tool \"([^\"]+)\"/)?.[1] + : undefined; const tool = - method === "item/fileChange/requestApproval" || method === "applyPatchApproval" + isMcpElicitation + ? (mcpTool ?? "mcp") + : method === "item/fileChange/requestApproval" || method === "applyPatchApproval" ? "edit" : isQuestion ? "ask_user" : "shell"; if (config.fullAuto && !isQuestion) { - return send({ jsonrpc: "2.0", id: msg.id, result: { decision: legacy ? "approved" : "accept" } }); + return send({ + jsonrpc: "2.0", + id: msg.id, + result: isMcpElicitation + ? { action: "accept", content: {} } + : { decision: legacy ? "approved" : "accept" }, + }); } const requestId = newId(); const summary = - typeof params.command === "string" + isMcpElicitation && typeof params.message === "string" + ? params.message + : typeof params.command === "string" ? params.command : Array.isArray(params.questions) ? params.questions.map((q: any) => q.question ?? q.header).filter(Boolean).join(" · ") @@ -284,7 +300,11 @@ export const CodexDriver: ProviderDriver = { send({ jsonrpc: "2.0", id: msg.id, - result: { decision: behavior === "allow" ? (legacy ? "approved" : "accept") : legacy ? "denied" : "decline" }, + result: isMcpElicitation + ? behavior === "allow" + ? { action: "accept", content: {} } + : { action: "decline" } + : { decision: behavior === "allow" ? (legacy ? "approved" : "accept") : legacy ? "denied" : "decline" }, }); } emit({ ...base(threadId, turnId), type: "request.resolved", requestId, behavior, source }); diff --git a/server/testing/fake-codex-app-server.ts b/server/testing/fake-codex-app-server.ts index d1017c928..da8730dba 100755 --- a/server/testing/fake-codex-app-server.ts +++ b/server/testing/fake-codex-app-server.ts @@ -5,7 +5,7 @@ // real app-server, it never exits on its own — the driver kills it. // // FAKE_CODEX_MODE happy (default) | approval | resume | stream | windows-command | -// logged-in-stdout | logged-out | unauthorized +// mcp-elicitation | logged-in-stdout | logged-out | unauthorized // FAKE_CODEX_DUMP path to write {argv, env, calls, decision} as JSON // // Keep this file dependency-free — it runs as a bare `node` subprocess. @@ -73,7 +73,7 @@ process.stdin.on("data", (chunk) => { } // response to our own server->client request (approval decision) - if (msg.id === 100 && (msg.result !== undefined || msg.error !== undefined)) { + if ((msg.id === 100 || msg.id === 101) && (msg.result !== undefined || msg.error !== undefined)) { decision = msg.result ?? { error: msg.error }; finishTurn(); continue; @@ -143,7 +143,20 @@ process.stdin.on("data", (chunk) => { : "ls -la"; notify("item/started", { item: { id: "i1", type: "commandExecution", command } }); notify("item/started", { item: { id: "w1", type: "webSearch", query: "OpenMausBot" } }); - if (mode === "approval" || mode === "windows-command") { + if (mode === "mcp-elicitation") { + out({ + jsonrpc: "2.0", + id: 101, + method: "mcpServer/elicitation/request", + params: { + serverName: "agents", + mode: "form", + _meta: { codex_approval_kind: "mcp_tool_call", tool_params: {} }, + message: 'Allow the agents MCP server to run tool "list_bots"?', + requestedSchema: { type: "object", properties: {} }, + }, + }); + } else if (mode === "approval" || mode === "windows-command") { const approvalCommand = mode === "windows-command" ? command : "rm -rf scratch"; out({ jsonrpc: "2.0", id: 100, method: "execCommandApproval", params: { command: approvalCommand } }); // turn continues from the approval response handler above