fix: route mac permissions through native agent requests
This commit is contained in:
@@ -2,6 +2,7 @@ import Cocoa
|
||||
import WebKit
|
||||
import ApplicationServices
|
||||
import AVFoundation
|
||||
import IOKit.hid
|
||||
import Network
|
||||
import UserNotifications
|
||||
|
||||
@@ -13,6 +14,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate {
|
||||
private var window: NSWindow?
|
||||
private var webView: WKWebView?
|
||||
private var inputMonitoringTap: CFMachPort?
|
||||
private var globalKeyMonitor: Any?
|
||||
private var inputMonitoringManager: IOHIDManager?
|
||||
private var localNetworkBrowser: NWBrowser?
|
||||
private var activeTab = "overview"
|
||||
|
||||
@@ -24,6 +27,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate {
|
||||
name: NSApplication.didBecomeActiveNotification,
|
||||
object: nil
|
||||
)
|
||||
NSAppleEventManager.shared().setEventHandler(
|
||||
self,
|
||||
andSelector: #selector(handleGetUrlEvent(_:withReplyEvent:)),
|
||||
forEventClass: AEEventClass(kInternetEventClass),
|
||||
andEventID: AEEventID(kAEGetURL)
|
||||
)
|
||||
|
||||
let webConfiguration = WKWebViewConfiguration()
|
||||
let webView = WKWebView(frame: .zero, configuration: webConfiguration)
|
||||
@@ -47,6 +56,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate {
|
||||
|
||||
loadAgentPanel(tab: activeTab)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
handleLaunchPermissionRequestIfNeeded()
|
||||
}
|
||||
|
||||
private func loadAgentPanel(tab: String? = nil) {
|
||||
@@ -68,6 +78,40 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate {
|
||||
loadAgentPanel(tab: activeTab)
|
||||
}
|
||||
|
||||
private func handleLaunchPermissionRequestIfNeeded() {
|
||||
let arguments = CommandLine.arguments
|
||||
guard
|
||||
let targetIndex = arguments.firstIndex(of: "--request-permission"),
|
||||
arguments.indices.contains(targetIndex + 1)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
let target = arguments[targetIndex + 1]
|
||||
let returnTab = commandLineValue(after: "--return-tab", in: arguments) ?? "permissions"
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in
|
||||
self?.handlePermissionTarget(target, returnTab: returnTab)
|
||||
}
|
||||
}
|
||||
|
||||
private func commandLineValue(after flag: String, in arguments: [String]) -> String? {
|
||||
guard let index = arguments.firstIndex(of: flag), arguments.indices.contains(index + 1) else {
|
||||
return nil
|
||||
}
|
||||
return arguments[index + 1]
|
||||
}
|
||||
|
||||
@objc private func handleGetUrlEvent(_ event: NSAppleEventDescriptor, withReplyEvent replyEvent: NSAppleEventDescriptor) {
|
||||
guard
|
||||
let urlString = event.paramDescriptor(forKeyword: keyDirectObject)?.stringValue,
|
||||
let url = URL(string: urlString),
|
||||
isBossAgentDeepLink(url)
|
||||
else {
|
||||
return
|
||||
}
|
||||
handleBossAgentDeepLink(url)
|
||||
}
|
||||
|
||||
func application(_ application: NSApplication, open urls: [URL]) {
|
||||
for url in urls where isBossAgentDeepLink(url) {
|
||||
handleBossAgentDeepLink(url)
|
||||
@@ -171,9 +215,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate {
|
||||
let returnTab = normalizedTab(rawReturnTab)
|
||||
activeTab = returnTab
|
||||
|
||||
UserDefaults.standard.set(target, forKey: "lastPermissionRequestTarget")
|
||||
UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: "lastPermissionRequestAt")
|
||||
NSLog("boss-agent permission request target=%@ returnTab=%@", target, returnTab)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
requestNativePermission(for: target)
|
||||
if let settingsUrl = systemSettingsUrl(for: target) {
|
||||
NSWorkspace.shared.open(settingsUrl)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.45) { [weak self] in
|
||||
if let settingsUrl = self?.systemSettingsUrl(for: target) {
|
||||
NSWorkspace.shared.open(settingsUrl)
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { [weak self] in
|
||||
@@ -185,6 +235,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate {
|
||||
[
|
||||
URLQueryItem(name: "nativeAccessibility", value: AXIsProcessTrusted() ? "granted" : "missing"),
|
||||
URLQueryItem(name: "nativeScreenRecording", value: screenRecordingStatus()),
|
||||
URLQueryItem(name: "nativeInputMonitoring", value: inputMonitoringStatus()),
|
||||
URLQueryItem(name: "nativeMicrophone", value: microphonePermissionStatus()),
|
||||
URLQueryItem(name: "nativeCamera", value: cameraPermissionStatus()),
|
||||
]
|
||||
@@ -223,6 +274,21 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private func inputMonitoringStatus() -> String {
|
||||
if #available(macOS 10.15, *) {
|
||||
return CGPreflightListenEventAccess() ? "granted" : "missing"
|
||||
}
|
||||
|
||||
switch IOHIDCheckAccess(kIOHIDRequestTypeListenEvent) {
|
||||
case kIOHIDAccessTypeGranted:
|
||||
return "granted"
|
||||
case kIOHIDAccessTypeDenied:
|
||||
return "missing"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
private func requestNativePermission(for target: String) {
|
||||
let targets = target == "all"
|
||||
? [
|
||||
@@ -314,6 +380,20 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate {
|
||||
}
|
||||
|
||||
private func requestInputMonitoringPermission() {
|
||||
var listenRequestResult = false
|
||||
if #available(macOS 10.15, *) {
|
||||
listenRequestResult = CGRequestListenEventAccess()
|
||||
} else {
|
||||
listenRequestResult = IOHIDRequestAccess(kIOHIDRequestTypeListenEvent)
|
||||
}
|
||||
NSLog("boss-agent input monitoring listen request result=%@", listenRequestResult ? "true" : "false")
|
||||
|
||||
if globalKeyMonitor == nil {
|
||||
globalKeyMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.keyDown]) { _ in }
|
||||
}
|
||||
preflightKeyboardStateRead()
|
||||
primeInputMonitoringHidPath()
|
||||
|
||||
let eventMask = CGEventMask(1 << CGEventType.keyDown.rawValue)
|
||||
inputMonitoringTap = CGEvent.tapCreate(
|
||||
tap: .cgSessionEventTap,
|
||||
@@ -329,6 +409,24 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private func preflightKeyboardStateRead() {
|
||||
for keyCode in [CGKeyCode(0), CGKeyCode(1), CGKeyCode(49), CGKeyCode(53)] {
|
||||
_ = CGEventSource.keyState(.combinedSessionState, key: keyCode)
|
||||
}
|
||||
}
|
||||
|
||||
private func primeInputMonitoringHidPath() {
|
||||
let manager = IOHIDManagerCreate(kCFAllocatorDefault, IOOptionBits(kIOHIDOptionsTypeNone))
|
||||
let keyboardMatching = [
|
||||
kIOHIDDeviceUsagePageKey as String: kHIDPage_GenericDesktop,
|
||||
kIOHIDDeviceUsageKey as String: kHIDUsage_GD_Keyboard,
|
||||
] as CFDictionary
|
||||
IOHIDManagerSetDeviceMatching(manager, keyboardMatching)
|
||||
let openResult = IOHIDManagerOpen(manager, IOOptionBits(kIOHIDOptionsTypeNone))
|
||||
NSLog("boss-agent input monitoring hid open result=%d", openResult)
|
||||
inputMonitoringManager = manager
|
||||
}
|
||||
|
||||
private func requestNotificationPermission() {
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { _, _ in }
|
||||
}
|
||||
@@ -348,15 +446,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate {
|
||||
|
||||
private func systemSettingsUrl(for target: String) -> URL? {
|
||||
let mapping = [
|
||||
"all": "x-apple.systempreferences:com.apple.preference.security?Privacy",
|
||||
"accessibility": "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility",
|
||||
"screenRecording": "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture",
|
||||
"automation": "x-apple.systempreferences:com.apple.preference.security?Privacy_Automation",
|
||||
"fullDiskAccess": "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles",
|
||||
"inputMonitoring": "x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent",
|
||||
"all": "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Security",
|
||||
"accessibility": "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Accessibility",
|
||||
"screenRecording": "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_ScreenCapture",
|
||||
"automation": "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Automation",
|
||||
"fullDiskAccess": "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_AllFiles",
|
||||
"inputMonitoring": "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_ListenEvent",
|
||||
"notifications": "x-apple.systempreferences:com.apple.Notifications-Settings.extension",
|
||||
"microphone": "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone",
|
||||
"camera": "x-apple.systempreferences:com.apple.preference.security?Privacy_Camera",
|
||||
"microphone": "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Microphone",
|
||||
"camera": "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Camera",
|
||||
"localNetwork": "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?privacy-localnetwork",
|
||||
]
|
||||
return URL(string: mapping[target] ?? mapping["all"]!)
|
||||
|
||||
@@ -64,15 +64,15 @@ const EXTENDED_PERMISSION_DEFS = [
|
||||
];
|
||||
|
||||
const MACOS_PERMISSION_SETTINGS = {
|
||||
all: "x-apple.systempreferences:com.apple.preference.security?Privacy",
|
||||
accessibility: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility",
|
||||
screenRecording: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture",
|
||||
automation: "x-apple.systempreferences:com.apple.preference.security?Privacy_Automation",
|
||||
fullDiskAccess: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles",
|
||||
inputMonitoring: "x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent",
|
||||
all: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Security",
|
||||
accessibility: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Accessibility",
|
||||
screenRecording: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_ScreenCapture",
|
||||
automation: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Automation",
|
||||
fullDiskAccess: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_AllFiles",
|
||||
inputMonitoring: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_ListenEvent",
|
||||
notifications: "x-apple.systempreferences:com.apple.Notifications-Settings.extension",
|
||||
microphone: "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone",
|
||||
camera: "x-apple.systempreferences:com.apple.preference.security?Privacy_Camera",
|
||||
microphone: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Microphone",
|
||||
camera: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Camera",
|
||||
localNetwork: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?privacy-localnetwork",
|
||||
};
|
||||
|
||||
@@ -81,6 +81,7 @@ const AUTO_PREFLIGHT_PERMISSION_KEYS = new Set(["accessibility", "screenRecordin
|
||||
const NATIVE_PERMISSION_QUERY_PARAMS = {
|
||||
accessibility: "nativeAccessibility",
|
||||
screenRecording: "nativeScreenRecording",
|
||||
inputMonitoring: "nativeInputMonitoring",
|
||||
microphone: "nativeMicrophone",
|
||||
camera: "nativeCamera",
|
||||
};
|
||||
@@ -885,12 +886,59 @@ export async function openBossAgentPermissionSettings(target = "all", platform =
|
||||
};
|
||||
}
|
||||
|
||||
const nativeUrl = `boss-agent://permissions/open?target=${encodeURIComponent(target)}&returnTab=permissions`;
|
||||
const nativeDeepLink = await runCommand("open", ["-b", "com.hyzq.boss.agent", nativeUrl], 2500);
|
||||
if (nativeDeepLink.ok) {
|
||||
return {
|
||||
ok: true,
|
||||
target,
|
||||
settingsUrl,
|
||||
message: "已通过 boss-agent 发起系统权限申请。",
|
||||
nativeRequest: true,
|
||||
nativeUrl,
|
||||
};
|
||||
}
|
||||
|
||||
const nativeLaunch = await runCommand(
|
||||
"open",
|
||||
[
|
||||
"-a",
|
||||
"/Applications/boss-agent.app",
|
||||
"--args",
|
||||
"--request-permission",
|
||||
target,
|
||||
"--return-tab",
|
||||
"permissions",
|
||||
],
|
||||
2500,
|
||||
);
|
||||
if (nativeLaunch.ok) {
|
||||
return {
|
||||
ok: true,
|
||||
target,
|
||||
settingsUrl,
|
||||
message: "已通过 boss-agent 发起系统权限申请。",
|
||||
nativeRequest: true,
|
||||
nativeUrl,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await runCommand("open", [settingsUrl], 2500);
|
||||
return {
|
||||
ok: result.ok,
|
||||
target,
|
||||
settingsUrl,
|
||||
message: result.ok ? "已打开系统权限设置。" : result.stderr || result.stdout || "打开系统权限设置失败。",
|
||||
message: result.ok
|
||||
? "已打开系统权限设置。"
|
||||
: nativeDeepLink.stderr
|
||||
|| nativeDeepLink.stdout
|
||||
|| nativeLaunch.stderr
|
||||
|| nativeLaunch.stdout
|
||||
|| result.stderr
|
||||
|| result.stdout
|
||||
|| "打开系统权限设置失败。",
|
||||
nativeRequest: false,
|
||||
nativeUrl,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ swiftc "$SOURCE_FILE" \
|
||||
-framework WebKit \
|
||||
-framework ApplicationServices \
|
||||
-framework AVFoundation \
|
||||
-framework IOKit \
|
||||
-framework Network \
|
||||
-framework UserNotifications
|
||||
|
||||
@@ -157,6 +158,8 @@ cat > "$CONTENTS_DIR/Info.plist" <<'PLIST'
|
||||
<string>boss-agent 需要通过自动化控制 Finder、浏览器和授权的企业应用,以完成远程桌面级任务。</string>
|
||||
<key>NSScreenCaptureUsageDescription</key>
|
||||
<string>boss-agent 需要读取屏幕画面,用于识别桌面状态、系统弹窗和远程控制结果。</string>
|
||||
<key>NSInputMonitoringUsageDescription</key>
|
||||
<string>boss-agent 需要输入监控权限,用于低层热键、复杂输入和部分不可访问控件兜底。</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>boss-agent 需要麦克风权限,用于语音协作和远程指令输入。</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
|
||||
@@ -67,6 +67,14 @@ test("boss-agent status exposes unbound QR binding and local permission states",
|
||||
status.permissionSetup.actions.find((action) => action.key === "localNetwork")?.settingsUrl,
|
||||
"x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?privacy-localnetwork",
|
||||
);
|
||||
assert.equal(
|
||||
status.permissionSetup.actions.find((action) => action.key === "inputMonitoring")?.settingsUrl,
|
||||
"x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_ListenEvent",
|
||||
);
|
||||
assert.equal(
|
||||
status.permissionSetup.actions.find((action) => action.key === "screenRecording")?.settingsUrl,
|
||||
"x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_ScreenCapture",
|
||||
);
|
||||
assert.deepEqual(
|
||||
status.permissions.items.map((item) => [item.key, item.status]),
|
||||
[
|
||||
@@ -199,7 +207,7 @@ test("boss-agent native permission overrides update app-owned camera and microph
|
||||
nativeCamera: "granted",
|
||||
nativeMicrophone: "missing",
|
||||
nativeScreenRecording: "granted",
|
||||
nativeInputMonitoring: "invalid",
|
||||
nativeInputMonitoring: "granted",
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -209,6 +217,7 @@ test("boss-agent native permission overrides update app-owned camera and microph
|
||||
automation: "granted",
|
||||
camera: "granted",
|
||||
microphone: "missing",
|
||||
inputMonitoring: "granted",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -240,19 +249,39 @@ test("boss-agent mac app intercepts permission links and triggers native app per
|
||||
|
||||
assert.match(swiftSource, /decidePolicyFor navigationAction/);
|
||||
assert.match(swiftSource, /api\/v1\/boss-agent\/permissions\/open/);
|
||||
assert.match(swiftSource, /NSApp\.activate\(ignoringOtherApps: true\)/);
|
||||
assert.match(swiftSource, /NSAppleEventManager\.shared\(\)\.setEventHandler/);
|
||||
assert.match(swiftSource, /kAEGetURL/);
|
||||
assert.match(swiftSource, /handleGetUrlEvent/);
|
||||
assert.match(swiftSource, /--request-permission/);
|
||||
assert.match(swiftSource, /--return-tab/);
|
||||
assert.match(swiftSource, /lastPermissionRequestTarget/);
|
||||
assert.match(swiftSource, /AXIsProcessTrustedWithOptions/);
|
||||
assert.match(swiftSource, /CGRequestScreenCaptureAccess/);
|
||||
assert.match(swiftSource, /AVCaptureDevice\.requestAccess\(for: \.audio/);
|
||||
assert.match(swiftSource, /AVCaptureDevice\.requestAccess\(for: \.video/);
|
||||
assert.match(swiftSource, /UNUserNotificationCenter\.current\(\)\.requestAuthorization/);
|
||||
assert.match(swiftSource, /import IOKit\.hid/);
|
||||
assert.match(swiftSource, /CGRequestListenEventAccess\(\)/);
|
||||
assert.match(swiftSource, /CGPreflightListenEventAccess\(\)/);
|
||||
assert.match(swiftSource, /NSEvent\.addGlobalMonitorForEvents\(matching: \[\.keyDown\]\)/);
|
||||
assert.match(swiftSource, /CGEventSource\.keyState\(\.combinedSessionState/);
|
||||
assert.match(swiftSource, /IOHIDManagerOpen\(manager/);
|
||||
assert.match(swiftSource, /kHIDUsage_GD_Keyboard/);
|
||||
assert.match(swiftSource, /IOHIDRequestAccess\(kIOHIDRequestTypeListenEvent\)/);
|
||||
assert.match(swiftSource, /IOHIDCheckAccess\(kIOHIDRequestTypeListenEvent\)/);
|
||||
assert.match(swiftSource, /CGEvent\.tapCreate/);
|
||||
assert.match(swiftSource, /NSWorkspace\.shared\.open/);
|
||||
assert.match(swiftSource, /deadline: \.now\(\) \+ 0\.45/);
|
||||
assert.match(swiftSource, /loadAgentPanel\(tab:/);
|
||||
assert.match(swiftSource, /returnTab/);
|
||||
assert.match(swiftSource, /nativePermissionQueryItems/);
|
||||
assert.match(swiftSource, /nativeCamera/);
|
||||
assert.match(swiftSource, /nativeMicrophone/);
|
||||
assert.match(swiftSource, /nativeInputMonitoring/);
|
||||
assert.match(swiftSource, /privacy-localnetwork/);
|
||||
assert.match(swiftSource, /com\.apple\.settings\.PrivacySecurity\.extension\?Privacy_ListenEvent/);
|
||||
assert.doesNotMatch(swiftSource, /com\.apple\.preference\.security\?Privacy_ListenEvent/);
|
||||
assert.match(swiftSource, /func application\(_ application: NSApplication, open urls: \[URL\]\)/);
|
||||
assert.match(swiftSource, /isBossAgentDeepLink/);
|
||||
assert.match(swiftSource, /AVCaptureDevice\.authorizationStatus\(for: \.video/);
|
||||
@@ -262,10 +291,19 @@ test("boss-agent mac app intercepts permission links and triggers native app per
|
||||
assert.match(buildScript, /NSCameraUsageDescription/);
|
||||
assert.match(buildScript, /NSAppleEventsUsageDescription/);
|
||||
assert.match(buildScript, /NSLocalNetworkUsageDescription/);
|
||||
assert.match(buildScript, /NSInputMonitoringUsageDescription/);
|
||||
assert.match(buildScript, /framework IOKit/);
|
||||
assert.match(buildScript, /CFBundleURLTypes/);
|
||||
assert.match(buildScript, /boss-agent/);
|
||||
assert.match(buildScript, /CFBundleIconFile/);
|
||||
assert.match(buildScript, /BossAgent\.icns/);
|
||||
assert.match(buildScript, /iconutil -c icns/);
|
||||
assert.match(buildScript, /codesign --force --deep --sign - "\$APP_DIR"/);
|
||||
|
||||
const statusSource = readFileSync("local-agent/boss-agent-status.mjs", "utf8");
|
||||
assert.match(statusSource, /boss-agent:\/\/permissions\/open/);
|
||||
assert.match(statusSource, /com\.hyzq\.boss\.agent/);
|
||||
assert.match(statusSource, /--request-permission/);
|
||||
assert.match(statusSource, /\/Applications\/boss-agent\.app/);
|
||||
assert.match(statusSource, /nativeRequest/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user