Files
hapi/cli/src/codex/utils/permissionHandler.ts
T
weishu dbcbc93ad0 feat: improve codex permission elicitation with parameter extraction and reason support
- Add robust parameter extraction helpers in codexMcpClient
- Implement buildElicitationResult to construct properly formatted MCP responses
- Support dynamic requested schemas with fallback handling
- Add reason/explanation tracking through permission decision flow
- Improve type safety with explicit extraction and validation
2025-12-22 09:27:20 +08:00

189 lines
6.4 KiB
TypeScript

/**
* Permission Handler for Codex tool approval integration
*
* Handles tool permission requests and responses for Codex sessions.
* Simpler than Claude's permission handler since we get tool IDs directly.
*/
import { logger } from "@/ui/logger";
import { ApiSessionClient } from "@/api/apiSession";
import { AgentState } from "@/api/types";
interface PermissionResponse {
id: string;
approved: boolean;
decision?: 'approved' | 'approved_for_session' | 'denied' | 'abort';
reason?: string;
}
interface PendingRequest {
resolve: (value: PermissionResult) => void;
reject: (error: Error) => void;
toolName: string;
input: unknown;
}
interface PermissionResult {
decision: 'approved' | 'approved_for_session' | 'denied' | 'abort';
reason?: string;
}
export class CodexPermissionHandler {
private pendingRequests = new Map<string, PendingRequest>();
private session: ApiSessionClient;
constructor(session: ApiSessionClient) {
this.session = session;
this.setupRpcHandler();
}
/**
* Handle a tool permission request
* @param toolCallId - The unique ID of the tool call
* @param toolName - The name of the tool being called
* @param input - The input parameters for the tool
* @returns Promise resolving to permission result
*/
async handleToolCall(
toolCallId: string,
toolName: string,
input: unknown
): Promise<PermissionResult> {
return new Promise<PermissionResult>((resolve, reject) => {
// Store the pending request
this.pendingRequests.set(toolCallId, {
resolve,
reject,
toolName,
input
});
// Send push notification
// this.session.api.push().sendToAllDevices(
// 'Permission Request',
// `Codex wants to use ${toolName}`,
// {
// sessionId: this.session.sessionId,
// requestId: toolCallId,
// tool: toolName,
// type: 'permission_request'
// }
// );
// Update agent state with pending request
this.session.updateAgentState((currentState) => ({
...currentState,
requests: {
...currentState.requests,
[toolCallId]: {
tool: toolName,
arguments: input,
createdAt: Date.now()
}
}
}));
logger.debug(`[Codex] Permission request sent for tool: ${toolName} (${toolCallId})`);
});
}
/**
* Setup RPC handler for permission responses
*/
private setupRpcHandler(): void {
this.session.rpcHandlerManager.registerHandler<PermissionResponse, void>(
'permission',
async (response) => {
// console.log(`[Codex] Permission response received:`, response);
const pending = this.pendingRequests.get(response.id);
if (!pending) {
logger.debug('[Codex] Permission request not found or already resolved');
return;
}
// Remove from pending
this.pendingRequests.delete(response.id);
// Resolve the permission request
const reason = typeof response.reason === 'string' ? response.reason : undefined;
const result: PermissionResult = response.approved
? {
decision: response.decision === 'approved_for_session' ? 'approved_for_session' : 'approved',
reason
}
: {
decision: response.decision === 'denied' ? 'denied' : 'abort',
reason
};
pending.resolve(result);
// Move request to completed in agent state
this.session.updateAgentState((currentState) => {
const request = currentState.requests?.[response.id];
if (!request) return currentState;
// console.log(`[Codex] Permission ${response.approved ? 'approved' : 'denied'} for ${pending.toolName}`);
const { [response.id]: _, ...remainingRequests } = currentState.requests || {};
let res = {
...currentState,
requests: remainingRequests,
completedRequests: {
...currentState.completedRequests,
[response.id]: {
...request,
completedAt: Date.now(),
status: response.approved ? 'approved' : 'denied',
decision: result.decision,
reason: result.reason
}
}
} satisfies AgentState;
// console.log(`[Codex] Updated agent state:`, res);
return res;
});
logger.debug(`[Codex] Permission ${response.approved ? 'approved' : 'denied'} for ${pending.toolName}`);
}
);
}
/**
* Reset state for new sessions
*/
reset(): void {
// Reject all pending requests
for (const [id, pending] of this.pendingRequests.entries()) {
pending.reject(new Error('Session reset'));
}
this.pendingRequests.clear();
// Clear requests in agent state
this.session.updateAgentState((currentState) => {
const pendingRequests = currentState.requests || {};
const completedRequests = { ...currentState.completedRequests };
// Move all pending to completed as canceled
for (const [id, request] of Object.entries(pendingRequests)) {
completedRequests[id] = {
...request,
completedAt: Date.now(),
status: 'canceled',
reason: 'Session reset'
};
}
return {
...currentState,
requests: {},
completedRequests
};
});
logger.debug('[Codex] Permission handler reset');
}
}