fix(codex): honor yolo for MCP elicitation (#655)

Route Codex app-server MCP elicitation decisions through the live session permission mode. HAPI bridge elicitation remains accepted, non-HAPI elicitation stays cancelled in non-yolo modes, and yolo accepts future non-HAPI prompts after mode changes.\n\nAdds adapter and launcher seam tests covering default -> yolo -> default behavior.\n\nTests:\n- bunx vitest run src/codex/utils/appServerPermissionAdapter.test.ts src/codex/codexRemoteLauncher.test.ts\n- bun run typecheck
This commit is contained in:
CoColate
2026-05-21 17:09:10 +08:00
committed by GitHub
parent 8e3bba9303
commit 611c4eec49
4 changed files with 162 additions and 7 deletions
@@ -189,6 +189,97 @@ describe('registerAppServerPermissionHandlers', () => {
});
});
it('accepts non-HAPI MCP elicitation requests when live permission mode is yolo', async () => {
const { client, handlers } = createClient();
let permissionMode: 'default' | 'read-only' | 'safe-yolo' | 'yolo' = 'default';
const permissionHandler = {
handleToolCall: vi.fn()
};
registerAppServerPermissionHandlers({
client: client as never,
permissionHandler: permissionHandler as never,
getPermissionMode: () => permissionMode
});
const handler = handlers.get('mcpServer/elicitation/request');
expect(handler).toBeTypeOf('function');
const request = {
threadId: 'thread-1',
turnId: 'turn-1',
serverName: 'qmd',
mode: 'form',
message: 'Allow the qmd MCP server to run tool "status"?',
_meta: null,
requestedSchema: {
type: 'object',
properties: {
approval: {
type: 'string',
enum: ['allow', 'deny']
}
},
required: ['approval']
}
};
await expect(handler?.(request)).resolves.toEqual({
action: 'cancel',
content: null,
_meta: null
});
permissionMode = 'yolo';
await expect(handler?.(request)).resolves.toEqual({
action: 'accept',
content: {
approval: 'allow'
},
_meta: null
});
permissionMode = 'default';
await expect(handler?.(request)).resolves.toEqual({
action: 'cancel',
content: null,
_meta: null
});
});
it('does not auto-accept non-HAPI MCP elicitation requests in safe-yolo mode', async () => {
const { client, handlers } = createClient();
const permissionHandler = {
handleToolCall: vi.fn()
};
registerAppServerPermissionHandlers({
client: client as never,
permissionHandler: permissionHandler as never,
getPermissionMode: () => 'safe-yolo'
});
const handler = handlers.get('mcpServer/elicitation/request');
expect(handler).toBeTypeOf('function');
await expect(handler?.({
threadId: 'thread-1',
turnId: 'turn-1',
serverName: 'external',
mode: 'form',
message: 'Collect data',
_meta: null,
requestedSchema: {
type: 'object',
properties: {},
}
})).resolves.toEqual({
action: 'cancel',
content: null,
_meta: null
});
});
it('cancels non-HAPI MCP elicitation requests', async () => {
const { client, handlers } = createClient();
const permissionHandler = {
@@ -1,5 +1,6 @@
import { randomUUID } from 'node:crypto';
import { logger } from '@/ui/logger';
import type { CodexPermissionMode } from '@hapi/protocol/types';
import type { CodexPermissionHandler } from './permissionHandler';
import type { CodexAppServerClient } from '../codexAppServerClient';
@@ -157,12 +158,13 @@ function isHapiBridgeElicitation(params: unknown): boolean {
export function registerAppServerPermissionHandlers(args: {
client: CodexAppServerClient;
permissionHandler: CodexPermissionHandler;
getPermissionMode?: () => CodexPermissionMode | undefined;
onUserInputRequest?: (request: { id: string; input: unknown }) => Promise<
| { decision: 'accept'; answers: Record<string, string[]> | Record<string, { answers: string[] }> }
| { decision: 'decline' | 'cancel' }
>;
}): void {
const { client, permissionHandler, onUserInputRequest } = args;
const { client, permissionHandler, getPermissionMode, onUserInputRequest } = args;
client.registerRequestHandler('item/commandExecution/requestApproval', async (params) => {
const record = asRecord(params) ?? {};
@@ -258,11 +260,15 @@ export function registerAppServerPermissionHandlers(args: {
client.registerRequestHandler('mcpServer/elicitation/request', async (params) => {
const record = asRecord(params) ?? {};
if (!isHapiBridgeElicitation(params)) {
const currentPermissionMode = getPermissionMode?.();
const shouldAccept = isHapiBridgeElicitation(params) || currentPermissionMode === 'yolo';
if (!shouldAccept) {
logger.debug('[CodexAppServer] Cancelling unsupported MCP elicitation request', {
serverName: record.serverName,
mode: record.mode,
message: record.message
message: record.message,
permissionMode: currentPermissionMode ?? 'unknown'
});
return {
@@ -275,7 +281,8 @@ export function registerAppServerPermissionHandlers(args: {
logger.debug('[CodexAppServer] Accepting MCP elicitation request', {
serverName: record.serverName,
mode: record.mode,
message: record.message
message: record.message,
permissionMode: currentPermissionMode ?? 'unknown'
});
return {