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.
This commit is contained in:
weishu
2026-01-13 11:06:50 +08:00
parent a6e35eaf65
commit 41d8844c00
4 changed files with 106 additions and 5 deletions
+1 -1
View File
@@ -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) {
+70 -2
View File
@@ -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<string, { command?: string; cwd?: string }>();
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<string, unknown> : {};
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()
});
}
+26 -2
View File
@@ -25,12 +25,27 @@ interface PermissionResult {
reason?: string;
}
export class CodexPermissionHandler extends BasePermissionHandler<PermissionResponse, PermissionResult> {
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<PermissionResponse, PermissionResult> {
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<PermissionResp
pending.resolve(result);
logger.debug(`[Codex] Permission ${response.approved ? 'approved' : 'denied'} for ${pending.toolName}`);
this.options?.onComplete?.({
id: response.id,
toolName: pending.toolName,
input: pending.input,
approved: response.approved,
decision: result.decision,
reason: result.reason
});
return {
status: response.approved ? 'approved' : 'denied',
decision: result.decision,