mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-08 01:43:07 +00:00
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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user