diff --git a/apps/macos/Sources/Clawdis/PermissionManager.swift b/apps/macos/Sources/Clawdis/PermissionManager.swift index d675e2aa7..38b66c77d 100644 --- a/apps/macos/Sources/Clawdis/PermissionManager.swift +++ b/apps/macos/Sources/Clawdis/PermissionManager.swift @@ -4,6 +4,7 @@ import AVFoundation import ClawdisIPC import CoreGraphics import Foundation +import OSLog import Speech import UserNotifications @@ -41,6 +42,13 @@ enum PermissionManager { 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 @@ -103,6 +111,9 @@ enum PermissionManager { 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() } @@ -139,6 +150,52 @@ enum NotificationPermissionHelper { } } +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 == -1_743 { // 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 { + _ = 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() diff --git a/apps/macos/Sources/Clawdis/VoiceWakeForwarder.swift b/apps/macos/Sources/Clawdis/VoiceWakeForwarder.swift index f8454f842..def01d8d9 100644 --- a/apps/macos/Sources/Clawdis/VoiceWakeForwarder.swift +++ b/apps/macos/Sources/Clawdis/VoiceWakeForwarder.swift @@ -53,6 +53,8 @@ enum VoiceWakeForwarder { let rendered = self.renderedCommand(template: config.commandTemplate, transcript: transcript) args.append(contentsOf: ["sh", "-c", rendered]) + self.logger.info("voice wake forward starting host=\(userHost, privacy: .public)") + let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/ssh") process.arguments = args @@ -75,7 +77,13 @@ enum VoiceWakeForwarder { } try? input.fileHandleForWriting.close() - _ = await self.wait(process, timeout: config.timeout) + let out = await self.wait(process, timeout: config.timeout) + if process.terminationStatus == 0 { + self.logger.info("voice wake forward ok host=\(userHost, privacy: .public)") + } else { + // swiftlint:disable:next line_length + self.logger.debug("voice wake forward exit=\(process.terminationStatus) host=\(userHost, privacy: .public) out=\(out, privacy: .public)") + } } static func checkConnection(config: VoiceWakeForwardConfig) async -> Result { diff --git a/apps/macos/Sources/Clawdis/VoiceWakeSettings.swift b/apps/macos/Sources/Clawdis/VoiceWakeSettings.swift index 85cd6879c..3ac1e1521 100644 --- a/apps/macos/Sources/Clawdis/VoiceWakeSettings.swift +++ b/apps/macos/Sources/Clawdis/VoiceWakeSettings.swift @@ -1,4 +1,5 @@ import AVFoundation +import OSLog import Speech import SwiftUI