/** * Blocks ACP dispatch on permission prompts until the user responds via API. */ import type { RequestPermissionRequest, RequestPermissionResponse } from '@agentclientprotocol/sdk'; import { isUnattendedMode } from './provider-manifest.js'; const DEFAULT_TIMEOUT_MS = 120_000; interface PendingPermission { request: RequestPermissionRequest; sessionId: string; resolve: (response: RequestPermissionResponse) => void; reject: (err: Error) => void; timer: ReturnType; } const pendingByTask = new Map(); export interface PermissionPrompt { taskId: string; toolTitle?: string; options: Array<{ optionId: string; label: string }>; } export interface PermissionHooks { onPrompt?: (prompt: PermissionPrompt & { sessionId: string }) => void | Promise; onResolved?: (taskId: string, sessionId: string) => void | Promise; } let hooks: PermissionHooks = {}; export function setPermissionHooks(next: PermissionHooks): void { hooks = next; } function toPrompt(taskId: string, params: RequestPermissionRequest): PermissionPrompt { return { taskId, toolTitle: params.toolCall?.title ?? undefined, options: params.options.map((o) => ({ optionId: o.optionId, label: o.name, })), }; } export function waitForPermissionResponse( taskId: string, sessionId: string, provider: string, modeId: string | undefined, params: RequestPermissionRequest, timeoutMs = DEFAULT_TIMEOUT_MS, ): Promise { if (isUnattendedMode(provider, modeId)) { const first = params.options[0]; if (first) { return Promise.resolve({ outcome: { outcome: 'selected', optionId: first.optionId } }); } return Promise.resolve({ outcome: { outcome: 'cancelled' } }); } return new Promise((resolve, reject) => { const existing = pendingByTask.get(taskId); if (existing) { clearTimeout(existing.timer); existing.reject(new Error('superseded by newer permission request')); } const timer = setTimeout(() => { pendingByTask.delete(taskId); void hooks.onResolved?.(taskId, sessionId); resolve({ outcome: { outcome: 'cancelled' } }); }, timeoutMs); pendingByTask.set(taskId, { request: params, sessionId, resolve, reject, timer }); const prompt = toPrompt(taskId, params); void hooks.onPrompt?.({ ...prompt, sessionId }); }); } export function respondToPermission(taskId: string, optionId: string | null): boolean { const pending = pendingByTask.get(taskId); if (!pending) return false; clearTimeout(pending.timer); pendingByTask.delete(taskId); if (optionId) { pending.resolve({ outcome: { outcome: 'selected', optionId } }); } else { pending.resolve({ outcome: { outcome: 'cancelled' } }); } void hooks.onResolved?.(taskId, pending.sessionId); return true; } export function getPendingPermission(taskId: string): PermissionPrompt | null { const pending = pendingByTask.get(taskId); if (!pending) return null; return toPrompt(taskId, pending.request); } export function cancelPendingPermission(taskId: string): void { const pending = pendingByTask.get(taskId); if (!pending) return; clearTimeout(pending.timer); pendingByTask.delete(taskId); pending.resolve({ outcome: { outcome: 'cancelled' } }); void hooks.onResolved?.(taskId, pending.sessionId); }