From 41d8844c00feb4fd77cc7aa1cb35b141b2930f4f Mon Sep 17 00:00:00 2001 From: weishu Date: Tue, 13 Jan 2026 10:25:15 +0800 Subject: [PATCH] fix: implement permission handler callbacks and add CodexPermission UI support Adds onRequest and onComplete callbacks to CodexPermissionHandler to track permission request/response lifecycle. Includes helper functions to normalize command and cwd inputs, and displays permission requests in the web UI. --- cli/src/codex/codexMcpClient.ts | 2 +- cli/src/codex/codexRemoteLauncher.ts | 72 +++++++++++++++++++++- cli/src/codex/utils/permissionHandler.ts | 28 ++++++++- web/src/components/ToolCard/knownTools.tsx | 9 +++ 4 files changed, 106 insertions(+), 5 deletions(-) diff --git a/cli/src/codex/codexMcpClient.ts b/cli/src/codex/codexMcpClient.ts index 47b8f905..23e719ea 100644 --- a/cli/src/codex/codexMcpClient.ts +++ b/cli/src/codex/codexMcpClient.ts @@ -248,7 +248,7 @@ export class CodexMcpClient { const toolCallId = extractToolCallId(params) ?? randomUUID(); const command = extractCommand(params); const cwd = extractCwd(params); - const toolName = 'CodexBash'; + const toolName = 'CodexPermission'; // If no permission handler set, deny by default if (!this.permissionHandler) { diff --git a/cli/src/codex/codexRemoteLauncher.ts b/cli/src/codex/codexRemoteLauncher.ts index 75b068bd..8e8b68a8 100644 --- a/cli/src/codex/codexRemoteLauncher.ts +++ b/cli/src/codex/codexRemoteLauncher.ts @@ -154,6 +154,38 @@ class CodexRemoteLauncher extends RemoteLauncherBase { const RESUME_CONTEXT_TOOL_MAX_CHARS = 2000; const RESUME_CONTEXT_REASONING_MAX_CHARS = 2000; + const normalizeCommand = (value: unknown): string | undefined => { + if (typeof value === 'string') { + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : undefined; + } + if (Array.isArray(value)) { + const joined = value.filter((part): part is string => typeof part === 'string').join(' '); + return joined.length > 0 ? joined : undefined; + } + return undefined; + }; + + const normalizeCwd = (value: unknown): string | undefined => { + if (typeof value !== 'string') return undefined; + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : undefined; + }; + + const permissionDetails = new Map(); + + const recordPermissionDetails = (id: string, command?: string, cwd?: string) => { + const existing = permissionDetails.get(id) ?? {}; + const next = { + command: command ?? existing.command, + cwd: cwd ?? existing.cwd + }; + permissionDetails.set(id, next); + return next; + }; + + const getPermissionDetails = (id: string) => permissionDetails.get(id) ?? {}; + function readResumeFileContent(resumeFile: string): { content: string; truncated: boolean } | null { try { const stat = fs.statSync(resumeFile); @@ -279,7 +311,43 @@ class CodexRemoteLauncher extends RemoteLauncherBase { return `${header}\n${rendered.join('\n')}`; } - const permissionHandler = new CodexPermissionHandler(session.client); + const permissionHandler = new CodexPermissionHandler(session.client, { + onRequest: ({ id, toolName, input }) => { + const inputRecord = input && typeof input === 'object' ? input as Record : {}; + const rawCommand = inputRecord.command; + const command = Array.isArray(rawCommand) + ? rawCommand.filter((part): part is string => typeof part === 'string').join(' ') + : typeof rawCommand === 'string' + ? rawCommand + : undefined; + const cwdValue = inputRecord.cwd; + const cwd = typeof cwdValue === 'string' && cwdValue.trim().length > 0 ? cwdValue : undefined; + + session.sendCodexMessage({ + type: 'tool-call', + name: 'CodexPermission', + callId: id, + input: { + tool: toolName, + command, + cwd + }, + id: randomUUID() + }); + }, + onComplete: ({ id, decision, reason, approved }) => { + session.sendCodexMessage({ + type: 'tool-call-result', + callId: id, + output: { + decision, + reason + }, + is_error: !approved, + id: randomUUID() + }); + } + }); const reasoningProcessor = new ReasoningProcessor((message) => { session.sendCodexMessage(message); }); @@ -368,7 +436,7 @@ class CodexRemoteLauncher extends RemoteLauncherBase { session.sendCodexMessage({ type: 'tool-call-result', callId: call_id, - output: output, + output, id: randomUUID() }); } diff --git a/cli/src/codex/utils/permissionHandler.ts b/cli/src/codex/utils/permissionHandler.ts index 80dbd474..b4a6ca06 100644 --- a/cli/src/codex/utils/permissionHandler.ts +++ b/cli/src/codex/utils/permissionHandler.ts @@ -25,12 +25,27 @@ interface PermissionResult { reason?: string; } -export class CodexPermissionHandler extends BasePermissionHandler { +type CodexPermissionHandlerOptions = { + onRequest?: (request: { id: string; toolName: string; input: unknown }) => void; + onComplete?: (result: { + id: string; + toolName: string; + input: unknown; + approved: boolean; + decision: PermissionResult['decision']; + reason?: string; + }) => void; +}; - constructor(session: ApiSessionClient) { +export class CodexPermissionHandler extends BasePermissionHandler { + constructor(session: ApiSessionClient, private readonly options?: CodexPermissionHandlerOptions) { super(session); } + protected override onRequestRegistered(id: string, toolName: string, input: unknown): void { + this.options?.onRequest?.({ id, toolName, input }); + } + /** * Handle a tool permission request * @param toolCallId - The unique ID of the tool call @@ -84,6 +99,15 @@ export class CodexPermissionHandler extends BasePermissionHandler , + title: (opts) => { + const tool = getInputStringAny(opts.input, ['tool']) + return tool ? `Permission: ${tool}` : 'Permission request' + }, + subtitle: (opts) => getInputStringAny(opts.input, ['command']) ?? null, + minimal: true + }, shell_command: { icon: () => , title: (opts) => opts.description ?? 'Terminal',