// // UIApprovalPresenter.swift // Zyquo Agent // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // // Bridges the PolicyEngine's ApprovalPresenting protocol to the UI: when the // gate holds an action, the presenter parks the engine on a // CheckedContinuation and publishes a PendingApproval to the RunController; // the inline approval card resolves it (Approve / Approve & remember / Edit / // Deny). Resolution is single-shot — a second resolve is ignored. // import Foundation /// One approval request surfaced to the UI, carrying its resolution hook. struct PendingApproval: Identifiable, Sendable { let id = UUID() let action: ActionRequest let risk: RiskAssessment private let resolver: ApprovalResolver init(action: ActionRequest, risk: RiskAssessment, continuation: CheckedContinuation) { self.action = action self.risk = risk self.resolver = ApprovalResolver(continuation: continuation) } /// Resolves the request exactly once; later calls are no-ops. func resolve(_ resolution: ApprovalResolution) { resolver.resolve(resolution) } } /// Single-shot continuation wrapper (thread-safe). private final class ApprovalResolver: @unchecked Sendable { private let lock = NSLock() private var continuation: CheckedContinuation? init(continuation: CheckedContinuation) { self.continuation = continuation } func resolve(_ resolution: ApprovalResolution) { lock.lock() let continuation = self.continuation self.continuation = nil lock.unlock() continuation?.resume(returning: resolution) } } /// The ApprovalPresenting implementation the UI wires into the PolicyEngine. /// `onRequest` is installed by the RunController and hops to the MainActor. final class UIApprovalPresenter: ApprovalPresenting, @unchecked Sendable { private let lock = NSLock() private var _onRequest: (@Sendable (PendingApproval) -> Void)? /// Installed by the RunController before the run starts. var onRequest: (@Sendable (PendingApproval) -> Void)? { get { lock.lock(); defer { lock.unlock() }; return _onRequest } set { lock.lock(); defer { lock.unlock() }; _onRequest = newValue } } func requestApproval(for action: ActionRequest, risk: RiskAssessment) async -> ApprovalResolution { guard let onRequest else { // No UI attached (should not happen in app runs): fail closed. return .deny } return await withCheckedContinuation { continuation in onRequest(PendingApproval(action: action, risk: risk, continuation: continuation)) } } }