VoiceWake: log detection, hold to 1s silence, ssh log clarity
This commit is contained in:
@@ -4,6 +4,7 @@ import AVFoundation
|
|||||||
import ClawdisIPC
|
import ClawdisIPC
|
||||||
import CoreGraphics
|
import CoreGraphics
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import OSLog
|
||||||
import Speech
|
import Speech
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
|
|
||||||
@@ -41,6 +42,13 @@ enum PermissionManager {
|
|||||||
results[cap] = false
|
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:
|
case .accessibility:
|
||||||
let trusted = await MainActor.run { AXIsProcessTrusted() }
|
let trusted = await MainActor.run { AXIsProcessTrusted() }
|
||||||
results[cap] = trusted
|
results[cap] = trusted
|
||||||
@@ -103,6 +111,9 @@ enum PermissionManager {
|
|||||||
results[cap] = settings.authorizationStatus == .authorized
|
results[cap] = settings.authorizationStatus == .authorized
|
||||||
|| settings.authorizationStatus == .provisional
|
|| settings.authorizationStatus == .provisional
|
||||||
|
|
||||||
|
case .appleScript:
|
||||||
|
results[cap] = await MainActor.run { AppleScriptPermission.isAuthorized() }
|
||||||
|
|
||||||
case .accessibility:
|
case .accessibility:
|
||||||
results[cap] = await MainActor.run { AXIsProcessTrusted() }
|
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
|
@MainActor
|
||||||
final class PermissionMonitor: ObservableObject {
|
final class PermissionMonitor: ObservableObject {
|
||||||
static let shared = PermissionMonitor()
|
static let shared = PermissionMonitor()
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ enum VoiceWakeForwarder {
|
|||||||
let rendered = self.renderedCommand(template: config.commandTemplate, transcript: transcript)
|
let rendered = self.renderedCommand(template: config.commandTemplate, transcript: transcript)
|
||||||
args.append(contentsOf: ["sh", "-c", rendered])
|
args.append(contentsOf: ["sh", "-c", rendered])
|
||||||
|
|
||||||
|
self.logger.info("voice wake forward starting host=\(userHost, privacy: .public)")
|
||||||
|
|
||||||
let process = Process()
|
let process = Process()
|
||||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/ssh")
|
process.executableURL = URL(fileURLWithPath: "/usr/bin/ssh")
|
||||||
process.arguments = args
|
process.arguments = args
|
||||||
@@ -75,7 +77,13 @@ enum VoiceWakeForwarder {
|
|||||||
}
|
}
|
||||||
try? input.fileHandleForWriting.close()
|
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<Void, VoiceWakeForwardError> {
|
static func checkConnection(config: VoiceWakeForwardConfig) async -> Result<Void, VoiceWakeForwardError> {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
import OSLog
|
||||||
import Speech
|
import Speech
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user