// Fake `codex app-server` fixture for appserver-connector.test.ts, spawned as // a child process with `bun run `. Speaks the app-server JSON-RPC // protocol over newline-delimited stdio with the same shapes the real binary // uses (verified against codex-rs/app-server-protocol v2), including: // // - handshake ordering: `thread/start` is refused until the `initialized` // notification has arrived, so the bridge's sequence is asserted here; // - a paginated `mcpServerStatus/list` whose FIRST page holds a different // server, so the bridge must follow `nextCursor`; // - a `needs_approval` tool that emits a server→client // `mcpServer/elicitation/request` and only succeeds when the answer is // an accept — the round trip through executor's elicitation bridge. import * as readline from "node:readline"; import { Option, Schema } from "effect"; const decodeMessage = Schema.decodeUnknownOption( Schema.fromJsonString( Schema.Struct({ id: Schema.optional(Schema.Union([Schema.Number, Schema.String])), method: Schema.optional(Schema.String), params: Schema.optional(Schema.Unknown), result: Schema.optional(Schema.Unknown), }), ), ); const decodeInitializeParams = Schema.decodeUnknownOption( Schema.Struct({ clientInfo: Schema.Struct({ name: Schema.String }) }), ); const decodeThreadStartParams = Schema.decodeUnknownOption( Schema.Struct({ approvalPolicy: Schema.optional(Schema.String) }), ); const decodeThreadParams = Schema.decodeUnknownOption( Schema.Struct({ threadId: Schema.optional(Schema.String), cursor: Schema.optional(Schema.String), }), ); const decodeToolCallParams = Schema.decodeUnknownOption( Schema.Struct({ threadId: Schema.String, server: Schema.String, tool: Schema.String, arguments: Schema.optional(Schema.Unknown), _meta: Schema.optional(Schema.Unknown), }), ); const decodeElicitAnswer = Schema.decodeUnknownOption( Schema.Struct({ action: Schema.String, content: Schema.optional(Schema.Unknown) }), ); const THREAD_ID = "thread-fixture-1"; const TOOLS = { echo: { name: "echo", description: "Echo the arguments back", inputSchema: { type: "object", properties: { text: { type: "string" } } }, }, announce_restart: { name: "announce_restart", description: "Emit server status notifications", inputSchema: { type: "object", properties: {} }, }, needs_approval: { name: "needs_approval", description: "Succeeds only after an accepted elicitation", inputSchema: { type: "object", properties: {} }, }, }; const write = (message: object): void => { process.stdout.write(`${JSON.stringify(message)}\n`); }; const reply = (id: number | string, result: unknown): void => { write({ jsonrpc: "2.0", id, result }); }; const replyError = (id: number | string, code: number, message: string): void => { write({ jsonrpc: "2.0", id, error: { code, message } }); }; let initializedSeen = false; let elicitationsAllowed = false; let nextServerRequestId = 1000; /** Elicitation request id → the pending tool call's request id. */ const pendingApprovals = new Map(); /** The `node_repl` server, as Codex exposes it: one raw `js` REPL tool. The * sky surface is projected onto this by the bridge, so the fixture only has * to echo back the program it was asked to run. */ const NODE_REPL_TOOLS = { js: { name: "js", description: "JavaScript code to execute with top-level await.", inputSchema: { type: "object", properties: { code: { type: "string" } } }, }, }; const serverStatusPage = (cursor: string | undefined): object => cursor === undefined ? { data: [ { name: "decoy", runtimeStatus: "connected", pluginId: null, serverInfo: null, tools: {}, resources: [], resourceTemplates: [], authStatus: "unsupported", }, ], nextCursor: "page-2", } : { data: [ { name: "messages", runtimeStatus: "connected", pluginId: "messages", serverInfo: null, tools: TOOLS, resources: [], resourceTemplates: [], authStatus: "unsupported", }, { name: "node_repl", runtimeStatus: "connected", pluginId: null, serverInfo: null, tools: NODE_REPL_TOOLS, resources: [], resourceTemplates: [], authStatus: "unsupported", }, ], nextCursor: null, }; const handleToolCall = (id: number | string, params: unknown): void => { const decoded = decodeToolCallParams(params); if (Option.isNone(decoded)) { replyError(id, -32602, "malformed mcpServer/tool/call params"); return; } const call = decoded.value; // `node_repl` echoes the program it was handed, so a test can assert what // the sky surface compiled without needing a real REPL. if (call.server === "node_repl") { const args = call.arguments as { code?: string } | undefined; // A Chrome-shaped per-site approval: no schema to fill in, and the terms // of the grant (`persist`, `origin`) carried in `_meta`. if (args?.code?.includes("__needs_site_approval")) { const elicitationId = nextServerRequestId++; pendingApprovals.set(elicitationId, id); write({ jsonrpc: "2.0", id: elicitationId, method: "mcpServer/elicitation/request", params: { threadId: THREAD_ID, turnId: null, serverName: "node_repl", mode: "form", message: "Allow Browser use to access https://example.com?", requestedSchema: { type: "object", properties: {} }, _meta: { connector_id: "browser-use", connector_name: "Browser use", origin: "https://example.com", persist: "always", }, }, }); return; } reply(id, { content: [{ type: "text", text: args?.code ?? "" }], // Echoed so a test can assert the turn metadata the Chrome client // requires, without needing a real browser. structuredContent: { meta: call._meta ?? null }, }); return; } if (call.threadId !== THREAD_ID || call.server !== "messages") { replyError(id, -32602, `unknown thread or server: ${call.threadId}/${call.server}`); return; } if (call.tool === "announce_restart") { // Codex reports a server's startup transitions; it names every server it // runs, so the fixture emits a decoy alongside the real one. write({ jsonrpc: "2.0", method: "mcpServer/startupStatus/updated", params: { threadId: THREAD_ID, name: "someone-else", status: "ready", error: null }, }); write({ jsonrpc: "2.0", method: "mcpServer/startupStatus/updated", params: { threadId: THREAD_ID, name: "messages", status: "starting", error: null }, }); write({ jsonrpc: "2.0", method: "mcpServer/startupStatus/updated", params: { threadId: THREAD_ID, name: "messages", status: "ready", error: null }, }); reply(id, { content: [{ type: "text", text: "announced" }] }); return; } if (call.tool === "echo") { reply(id, { content: [{ type: "text", text: JSON.stringify(call.arguments ?? {}) }], structuredContent: { codexHome: process.env["CODEX_HOME"] ?? null }, isError: null, }); return; } if (call.tool === "needs_approval") { if (!elicitationsAllowed) { // Exactly what Codex returns when it declines the elicitation for the // client: an error result, no prompt, no explanation. reply(id, { content: [{ type: "text", text: "access was not approved" }], isError: true, }); return; } const elicitationId = nextServerRequestId++; pendingApprovals.set(elicitationId, id); write({ jsonrpc: "2.0", id: elicitationId, method: "mcpServer/elicitation/request", params: { threadId: THREAD_ID, turnId: null, serverName: "messages", mode: "form", _meta: null, message: "Allow the fixture to proceed?", requestedSchema: { type: "object", properties: {} }, }, }); return; } replyError(id, -32602, `unknown tool: ${call.tool}`); }; const handleElicitationAnswer = (id: number | string, result: unknown): void => { const callId = pendingApprovals.get(id); if (callId === undefined) return; pendingApprovals.delete(id); const answer = Option.getOrUndefined(decodeElicitAnswer(result)); if (answer?.action === "accept") { reply(callId, { content: [{ type: "text", text: "approved" }] }); return; } reply(callId, { content: [{ type: "text", text: `denied: ${answer?.action ?? "unreadable"}` }], isError: true, }); }; readline.createInterface({ input: process.stdin }).on("line", (line) => { const decoded = decodeMessage(line); if (Option.isNone(decoded)) return; const message = decoded.value; if (message.method === undefined) { // A response from the bridge — only elicitation answers flow this way. if (message.id !== undefined) handleElicitationAnswer(message.id, message.result); return; } if (message.method === "initialized") { initializedSeen = true; return; } if (message.id === undefined) return; if (message.method === "initialize") { const params = decodeInitializeParams(message.params); if (Option.isNone(params)) { replyError(message.id, -32602, "initialize requires clientInfo"); return; } reply(message.id, { userAgent: "codex-fixture/0.0.0" }); return; } if (message.method === "thread/start") { if (!initializedSeen) { replyError(message.id, -32600, "thread/start before the initialized notification"); return; } // Codex DECLINES every MCP elicitation itself on a thread whose approval // policy does not allow them, so a thread started without one can never // receive an approval prompt. The fixture models that: it only elicits // when the bridge asked for a policy that permits elicitations. const params = Option.getOrUndefined(decodeThreadStartParams(message.params)); elicitationsAllowed = params?.approvalPolicy === "on-request"; reply(message.id, { thread: { id: THREAD_ID } }); return; } if (message.method === "mcpServerStatus/list") { const params = Option.getOrUndefined(decodeThreadParams(message.params)); if (params?.threadId !== THREAD_ID) { replyError(message.id, -32602, "mcpServerStatus/list requires the started threadId"); return; } reply(message.id, serverStatusPage(params.cursor)); return; } if (message.method === "mcpServer/tool/call") { handleToolCall(message.id, message.params); return; } replyError(message.id, -32601, `fixture does not implement ${message.method}`); });