import ClawdbotKit import ClawdbotProtocol import CoreGraphics import Foundation import OSLog @MainActor final class ExecApprovalsGatewayPrompter { static let shared = ExecApprovalsGatewayPrompter() private let logger = Logger(subsystem: "com.clawdbot", category: "exec-approvals.gateway") private var task: Task? struct GatewayApprovalRequest: Codable, Sendable { var id: String var request: ExecApprovalPromptRequest var createdAtMs: Int var expiresAtMs: Int } func start() { guard self.task == nil else { return } self.task = Task { [weak self] in await self?.run() } } func stop() { self.task?.cancel() self.task = nil } private func run() async { let stream = await GatewayConnection.shared.subscribe(bufferingNewest: 200) for await push in stream { if Task.isCancelled { return } await self.handle(push: push) } } private func handle(push: GatewayPush) async { guard case let .event(evt) = push else { return } guard evt.event == "exec.approval.requested" else { return } guard let payload = evt.payload else { return } do { let data = try JSONEncoder().encode(payload) let request = try JSONDecoder().decode(GatewayApprovalRequest.self, from: data) guard self.shouldPresent(request: request) else { return } let decision = ExecApprovalsPromptPresenter.prompt(request.request) try await GatewayConnection.shared.requestVoid( method: .execApprovalResolve, params: [ "id": AnyCodable(request.id), "decision": AnyCodable(decision.rawValue), ], timeoutMs: 10000) } catch { self.logger.error("exec approval handling failed \(error.localizedDescription, privacy: .public)") } } private func shouldPresent(request: GatewayApprovalRequest) -> Bool { let mode = AppStateStore.shared.connectionMode let activeSession = WebChatManager.shared.activeSessionKey?.trimmingCharacters(in: .whitespacesAndNewlines) let requestSession = request.request.sessionKey?.trimmingCharacters(in: .whitespacesAndNewlines) let recentlyActive = self.isRecentlyActive(mode: mode, thresholdSeconds: 120) if let session = requestSession, !session.isEmpty { if let active = activeSession, !active.isEmpty { return active == session } return recentlyActive } if let active = activeSession, !active.isEmpty { return true } return mode == .local } private func isRecentlyActive(mode: AppState.ConnectionMode, thresholdSeconds: Int) -> Bool { guard let seconds = Self.lastInputSeconds() else { return mode == .local } return seconds <= thresholdSeconds } private static func lastInputSeconds() -> Int? { let anyEvent = CGEventType(rawValue: UInt32.max) ?? .null let seconds = CGEventSource.secondsSinceLastEventType(.combinedSessionState, eventType: anyEvent) if seconds.isNaN || seconds.isInfinite || seconds < 0 { return nil } return Int(seconds.rounded()) } }