import { randomUUID } from "node:crypto"; import type { ExecApprovalDecision } from "../infra/exec-approvals.js"; export type ExecApprovalRequestPayload = { command: string; cwd?: string | null; host?: string | null; security?: string | null; ask?: string | null; agentId?: string | null; resolvedPath?: string | null; sessionKey?: string | null; }; export type ExecApprovalRecord = { id: string; request: ExecApprovalRequestPayload; createdAtMs: number; expiresAtMs: number; resolvedAtMs?: number; decision?: ExecApprovalDecision; resolvedBy?: string | null; }; type PendingEntry = { record: ExecApprovalRecord; resolve: (decision: ExecApprovalDecision) => void; reject: (err: Error) => void; timer: ReturnType; }; export class ExecApprovalManager { private pending = new Map(); create(request: ExecApprovalRequestPayload, timeoutMs: number): ExecApprovalRecord { const now = Date.now(); const id = randomUUID(); const record: ExecApprovalRecord = { id, request, createdAtMs: now, expiresAtMs: now + timeoutMs, }; return record; } async waitForDecision(record: ExecApprovalRecord, timeoutMs: number): Promise { return await new Promise((resolve, reject) => { const timer = setTimeout(() => { this.pending.delete(record.id); resolve("deny"); }, timeoutMs); this.pending.set(record.id, { record, resolve, reject, timer }); }); } resolve(recordId: string, decision: ExecApprovalDecision, resolvedBy?: string | null): boolean { const pending = this.pending.get(recordId); if (!pending) return false; clearTimeout(pending.timer); pending.record.resolvedAtMs = Date.now(); pending.record.decision = decision; pending.record.resolvedBy = resolvedBy ?? null; this.pending.delete(recordId); pending.resolve(decision); return true; } getSnapshot(recordId: string): ExecApprovalRecord | null { const entry = this.pending.get(recordId); return entry?.record ?? null; } }