mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
- 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
189 lines
6.4 KiB
TypeScript
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');
|
|
}
|
|
}
|