import AppKit import ApplicationServices import AVFoundation import ClawdisIPC import CoreGraphics import Foundation import OSLog import Speech import UserNotifications enum PermissionManager { static func ensure(_ caps: [Capability], interactive: Bool) async -> [Capability: Bool] { var results: [Capability: Bool] = [:] for cap in caps { switch cap { case .notifications: let center = UNUserNotificationCenter.current() let settings = await center.notificationSettings() switch settings.authorizationStatus { case .authorized, .provisional, .ephemeral: results[cap] = true case .notDetermined: if interactive { let granted = await (try? center.requestAuthorization(options: [.alert, .sound, .badge])) ?? false let updated = await center.notificationSettings() results[cap] = granted && (updated.authorizationStatus == .authorized || updated .authorizationStatus == .provisional) } else { results[cap] = false } case .denied: results[cap] = false if interactive { NotificationPermissionHelper.openSettings() } @unknown default: results[cap] = false } case .appleScript: let granted = await MainActor.run { AppleScriptPermission.isAuthorized() } if interactive, !granted { await AppleScriptPermission.requestAuthorization() } results[cap] = await MainActor.run { AppleScriptPermission.isAuthorized() } case .accessibility: let trusted = await MainActor.run { AXIsProcessTrusted() } results[cap] = trusted if interactive, !trusted { await MainActor.run { let opts: NSDictionary = ["AXTrustedCheckOptionPrompt": true] _ = AXIsProcessTrustedWithOptions(opts) } } case .screenRecording: let granted = ScreenRecordingProbe.isAuthorized() if interactive, !granted { await ScreenRecordingProbe.requestAuthorization() } results[cap] = ScreenRecordingProbe.isAuthorized() case .microphone: let granted = AVCaptureDevice.authorizationStatus(for: .audio) == .authorized if interactive, !granted { let ok = await AVCaptureDevice.requestAccess(for: .audio) results[cap] = ok } else { results[cap] = granted } case .speechRecognition: let status = SFSpeechRecognizer.authorizationStatus() if status == .notDetermined, interactive { await withUnsafeContinuation { (cont: UnsafeContinuation) in SFSpeechRecognizer.requestAuthorization { _ in DispatchQueue.main.async { cont.resume() } } } } results[cap] = SFSpeechRecognizer.authorizationStatus() == .authorized } } return results } static func voiceWakePermissionsGranted() -> Bool { let mic = AVCaptureDevice.authorizationStatus(for: .audio) == .authorized let speech = SFSpeechRecognizer.authorizationStatus() == .authorized return mic && speech } static func ensureVoiceWakePermissions(interactive: Bool) async -> Bool { let results = await self.ensure([.microphone, .speechRecognition], interactive: interactive) return results[.microphone] == true && results[.speechRecognition] == true } static func status(_ caps: [Capability] = Capability.allCases) async -> [Capability: Bool] { var results: [Capability: Bool] = [:] for cap in caps { switch cap { case .notifications: let center = UNUserNotificationCenter.current() let settings = await center.notificationSettings() results[cap] = settings.authorizationStatus == .authorized || settings.authorizationStatus == .provisional case .appleScript: results[cap] = await MainActor.run { AppleScriptPermission.isAuthorized() } case .accessibility: results[cap] = await MainActor.run { AXIsProcessTrusted() } case .screenRecording: if #available(macOS 10.15, *) { results[cap] = CGPreflightScreenCaptureAccess() } else { results[cap] = true } case .microphone: results[cap] = AVCaptureDevice.authorizationStatus(for: .audio) == .authorized case .speechRecognition: results[cap] = SFSpeechRecognizer.authorizationStatus() == .authorized } } return results } } enum NotificationPermissionHelper { static func openSettings() { let candidates = [ "x-apple.systempreferences:com.apple.Notifications-Settings.extension", "x-apple.systempreferences:com.apple.preference.notifications", ] for candidate in candidates { if let url = URL(string: candidate), NSWorkspace.shared.open(url) { return } } } } enum AppleScriptPermission { private static let logger = Logger(subsystem: "com.steipete.clawdis", category: "AppleScriptPermission") /// Sends a benign AppleScript to Terminal to verify Automation permission. @MainActor static func isAuthorized() -> Bool { let script = """ tell application "Terminal" return "clawdis-ok" end tell """ var error: NSDictionary? let appleScript = NSAppleScript(source: script) let result = appleScript?.executeAndReturnError(&error) if let error, let code = error["NSAppleScriptErrorNumber"] as? Int { if code == -1743 { // errAEEventWouldRequireUserConsent Self.logger.debug("AppleScript permission denied (-1743)") return false } Self.logger.debug("AppleScript check failed with code \(code)") } return result != nil } /// Triggers the TCC prompt and opens System Settings → Privacy & Security → Automation. @MainActor static func requestAuthorization() async { _ = self.isAuthorized() // first attempt triggers the dialog if not granted // Open the Automation pane to help the user if the prompt was dismissed. let urlStrings = [ "x-apple.systempreferences:com.apple.preference.security?Privacy_Automation", "x-apple.systempreferences:com.apple.preference.security", ] for candidate in urlStrings { if let url = URL(string: candidate), NSWorkspace.shared.open(url) { break } } } } @MainActor final class PermissionMonitor: ObservableObject { static let shared = PermissionMonitor() @Published private(set) var status: [Capability: Bool] = [:] private var monitorTimer: Timer? private var isChecking = false private var registrations = 0 private var lastCheck: Date? private let minimumCheckInterval: TimeInterval = 0.5 func register() { self.registrations += 1 if self.registrations == 1 { self.startMonitoring() } } func unregister() { guard self.registrations > 0 else { return } self.registrations -= 1 if self.registrations == 0 { self.stopMonitoring() } } func refreshNow() async { await self.checkStatus(force: true) } private func startMonitoring() { Task { await self.checkStatus(force: true) } self.monitorTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in guard let self else { return } Task { @MainActor in await self.checkStatus(force: false) } } } private func stopMonitoring() { self.monitorTimer?.invalidate() self.monitorTimer = nil self.lastCheck = nil } private func checkStatus(force: Bool) async { if self.isChecking { return } let now = Date() if !force, let lastCheck, now.timeIntervalSince(lastCheck) < self.minimumCheckInterval { return } self.isChecking = true let latest = await PermissionManager.status() if latest != self.status { self.status = latest } self.lastCheck = Date() self.isChecking = false } } enum ScreenRecordingProbe { static func isAuthorized() -> Bool { if #available(macOS 10.15, *) { return CGPreflightScreenCaptureAccess() } return true } @MainActor static func requestAuthorization() async { if #available(macOS 10.15, *) { _ = CGRequestScreenCaptureAccess() } } }