fix: align agent permissions with native app
This commit is contained in:
@@ -247,7 +247,7 @@ open dist/boss-agent.app
|
||||
- `boss-agent.app` 是本机 `local-agent` 的 macOS WebView 外壳,默认打开 `http://127.0.0.1:4317/boss-agent`
|
||||
- 未绑定账号时会显示可扫码的 Boss APP 绑定二维码;已绑定后显示账号、API、服务器、授权、本机权限获取和本机 Skill 部署情况
|
||||
- 本机权限会区分两级判断:`辅助功能 / 屏幕录制 / 自动化控制` 只代表核心桌面控制能力;完整接管还需要按业务场景补齐全磁盘访问、输入监控、通知、麦克风、摄像头和本地网络等权限
|
||||
- 本机权限页提供“一次完整授权”入口,点击后通过 `GET /api/v1/boss-agent/permissions/open?target=all` 打开 macOS 隐私设置;各权限项也有独立设置入口。授权完成后由系统持久保存,后续控制过程只静默校验并使用,不在任务执行中临时申请
|
||||
- 本机权限页提供“一次完整授权”入口;在 `boss-agent.app` 内点击时会先由应用本体触发辅助功能、屏幕录制、自动化、输入监控、通知、麦克风、摄像头和本地网络等原生权限预检,再打开对应 macOS 隐私设置页,确保权限列表里出现 `boss-agent` 供用户直接开启。授权完成后由系统持久保存,后续控制过程只静默校验并使用,不在任务执行中临时申请
|
||||
- 本机状态 JSON 可通过 `GET http://127.0.0.1:4317/api/v1/boss-agent/status` 查看,不会返回设备 token 明文
|
||||
|
||||
device-agent 当前职责:
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import Cocoa
|
||||
import WebKit
|
||||
import ApplicationServices
|
||||
import AVFoundation
|
||||
import Network
|
||||
import UserNotifications
|
||||
|
||||
private let bossInputMonitoringTapCallback: CGEventTapCallBack = { _, _, event, _ in
|
||||
Unmanaged.passUnretained(event)
|
||||
}
|
||||
|
||||
final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate {
|
||||
private var window: NSWindow?
|
||||
private var webView: WKWebView?
|
||||
private var inputMonitoringTap: CFMachPort?
|
||||
private var localNetworkBrowser: NWBrowser?
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
NSApp.setActivationPolicy(.regular)
|
||||
@@ -48,6 +58,173 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate {
|
||||
loadFallback()
|
||||
}
|
||||
|
||||
func webView(
|
||||
_ webView: WKWebView,
|
||||
decidePolicyFor navigationAction: WKNavigationAction,
|
||||
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
|
||||
) {
|
||||
guard let url = navigationAction.request.url else {
|
||||
decisionHandler(.allow)
|
||||
return
|
||||
}
|
||||
|
||||
if isPermissionSetupUrl(url) {
|
||||
decisionHandler(.cancel)
|
||||
handlePermissionSetupNavigation(url)
|
||||
return
|
||||
}
|
||||
|
||||
decisionHandler(.allow)
|
||||
}
|
||||
|
||||
private func isPermissionSetupUrl(_ url: URL) -> Bool {
|
||||
url.path == "/api/v1/boss-agent/permissions/open"
|
||||
}
|
||||
|
||||
private func handlePermissionSetupNavigation(_ url: URL) {
|
||||
let target =
|
||||
URLComponents(url: url, resolvingAgainstBaseURL: false)?
|
||||
.queryItems?
|
||||
.first(where: { $0.name == "target" })?
|
||||
.value ?? "all"
|
||||
|
||||
requestNativePermission(for: target)
|
||||
if let settingsUrl = systemSettingsUrl(for: target) {
|
||||
NSWorkspace.shared.open(settingsUrl)
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { [weak self] in
|
||||
self?.loadAgentPanel()
|
||||
}
|
||||
}
|
||||
|
||||
private func requestNativePermission(for target: String) {
|
||||
let targets = target == "all"
|
||||
? [
|
||||
"accessibility",
|
||||
"screenRecording",
|
||||
"automation",
|
||||
"fullDiskAccess",
|
||||
"inputMonitoring",
|
||||
"notifications",
|
||||
"microphone",
|
||||
"camera",
|
||||
"localNetwork",
|
||||
]
|
||||
: [target]
|
||||
|
||||
for permission in targets {
|
||||
requestSingleNativePermission(permission)
|
||||
}
|
||||
}
|
||||
|
||||
private func requestSingleNativePermission(_ permission: String) {
|
||||
switch permission {
|
||||
case "accessibility":
|
||||
requestAccessibilityPermission()
|
||||
case "screenRecording":
|
||||
requestScreenRecordingPermission()
|
||||
case "automation":
|
||||
requestAutomationPermission()
|
||||
case "fullDiskAccess":
|
||||
preflightFullDiskAccess()
|
||||
case "inputMonitoring":
|
||||
requestInputMonitoringPermission()
|
||||
case "notifications":
|
||||
requestNotificationPermission()
|
||||
case "microphone":
|
||||
AVCaptureDevice.requestAccess(for: .audio) { _ in }
|
||||
case "camera":
|
||||
AVCaptureDevice.requestAccess(for: .video) { _ in }
|
||||
case "localNetwork":
|
||||
requestLocalNetworkPermission()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func requestAccessibilityPermission() {
|
||||
let promptKey = kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String
|
||||
let options = [promptKey: true] as CFDictionary
|
||||
_ = AXIsProcessTrustedWithOptions(options)
|
||||
}
|
||||
|
||||
private func requestScreenRecordingPermission() {
|
||||
if #available(macOS 10.15, *) {
|
||||
_ = CGRequestScreenCaptureAccess()
|
||||
} else {
|
||||
_ = CGPreflightScreenCaptureAccess()
|
||||
}
|
||||
}
|
||||
|
||||
private func requestAutomationPermission() {
|
||||
let script = NSAppleScript(source: "tell application \"Finder\" to get name")
|
||||
var error: NSDictionary?
|
||||
_ = script?.executeAndReturnError(&error)
|
||||
}
|
||||
|
||||
private func preflightFullDiskAccess() {
|
||||
let candidatePaths = [
|
||||
"\(NSHomeDirectory())/Library/Safari/Bookmarks.plist",
|
||||
"\(NSHomeDirectory())/Library/Mail",
|
||||
"/Library/Application Support/com.apple.TCC/TCC.db",
|
||||
]
|
||||
for path in candidatePaths {
|
||||
if FileManager.default.fileExists(atPath: path) {
|
||||
_ = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func requestInputMonitoringPermission() {
|
||||
let eventMask = CGEventMask(1 << CGEventType.keyDown.rawValue)
|
||||
inputMonitoringTap = CGEvent.tapCreate(
|
||||
tap: .cgSessionEventTap,
|
||||
place: .headInsertEventTap,
|
||||
options: .listenOnly,
|
||||
eventsOfInterest: eventMask,
|
||||
callback: bossInputMonitoringTapCallback,
|
||||
userInfo: nil
|
||||
)
|
||||
if let tap = inputMonitoringTap {
|
||||
CFMachPortInvalidate(tap)
|
||||
inputMonitoringTap = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func requestNotificationPermission() {
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { _, _ in }
|
||||
}
|
||||
|
||||
private func requestLocalNetworkPermission() {
|
||||
let parameters = NWParameters.tcp
|
||||
parameters.includePeerToPeer = true
|
||||
let browser = NWBrowser(for: .bonjour(type: "_http._tcp", domain: nil), using: parameters)
|
||||
browser.stateUpdateHandler = { _ in }
|
||||
localNetworkBrowser = browser
|
||||
browser.start(queue: .main)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { [weak self] in
|
||||
self?.localNetworkBrowser?.cancel()
|
||||
self?.localNetworkBrowser = nil
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
"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",
|
||||
"localNetwork": "x-apple.systempreferences:com.apple.preference.security?Privacy_LocalNetwork",
|
||||
]
|
||||
return URL(string: mapping[target] ?? mapping["all"]!)
|
||||
}
|
||||
|
||||
private func loadFallback() {
|
||||
let html = """
|
||||
<!doctype html>
|
||||
|
||||
@@ -221,7 +221,7 @@ function buildPermissionSetupPlan(coreItems, extendedItems, readiness) {
|
||||
canPreflight: AUTO_PREFLIGHT_PERMISSION_KEYS.has(item.key),
|
||||
settingsUrl: MACOS_PERMISSION_SETTINGS[item.key] ?? MACOS_PERMISSION_SETTINGS.all,
|
||||
openUrl: `/api/v1/boss-agent/permissions/open?target=${encodeURIComponent(item.key)}`,
|
||||
owner: "boss-agent / local-agent",
|
||||
owner: "boss-agent.app",
|
||||
}));
|
||||
const missingActions = actions.filter((action) => action.status !== "granted");
|
||||
|
||||
@@ -350,46 +350,6 @@ function permissionText(status) {
|
||||
return "待确认";
|
||||
}
|
||||
|
||||
function permissionRows(items) {
|
||||
return items
|
||||
.map((item) => {
|
||||
const tone = statusTone(item.status);
|
||||
return `<div class="permission-row">
|
||||
<div>
|
||||
<div class="permission-name">${escapeHtml(item.label)}</div>
|
||||
<div class="muted">${escapeHtml(item.description)}</div>
|
||||
</div>
|
||||
<span class="pill ${tone}">${escapeHtml(permissionText(item.status))}</span>
|
||||
</div>`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
function sidebarPermissionBlock(status) {
|
||||
const readiness = status.permissionReadiness;
|
||||
const coreTone = readiness.coreReady ? "good" : "bad";
|
||||
const fullTone = readiness.fullControlReady ? "good" : "warn";
|
||||
const missingExtended = status.permissions.extendedItems
|
||||
.filter((item) => item.status !== "granted")
|
||||
.map((item) => item.label)
|
||||
.join("、");
|
||||
return `<section class="sidebar-card">
|
||||
<div class="sidebar-title">本机权限获取</div>
|
||||
<div class="sidebar-note">${escapeHtml(readiness.summary)}</div>
|
||||
<div class="sidebar-kv">
|
||||
<span>核心桌面控制</span>
|
||||
<b class="${coreTone}">${escapeHtml(`${readiness.coreGrantedCount}/${readiness.coreTotal}`)}</b>
|
||||
</div>
|
||||
<div class="sidebar-kv">
|
||||
<span>完整接管权限</span>
|
||||
<b class="${fullTone}">${escapeHtml(`${readiness.extendedGrantedCount}/${readiness.extendedTotal}`)}</b>
|
||||
</div>
|
||||
<div class="sidebar-note">${escapeHtml(missingExtended ? `完整接管还需:${missingExtended}` : "完整接管权限已满足")}</div>
|
||||
<a class="sidebar-action" href="${escapeHtml(status.permissionSetup.primaryAction.href)}">${escapeHtml(status.permissionSetup.title)}</a>
|
||||
<div class="sidebar-mini-list">${permissionRows(status.permissions.items)}</div>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
function setupActionRows(status) {
|
||||
return status.permissionSetup.actions
|
||||
.map((action) => {
|
||||
@@ -429,22 +389,6 @@ function skillRows(status) {
|
||||
.join("");
|
||||
}
|
||||
|
||||
function sidebarSkillBlock(status) {
|
||||
const syncTone = status.skills?.syncOk ? "good" : "warn";
|
||||
return `<section class="sidebar-card">
|
||||
<div class="sidebar-title">Skill</div>
|
||||
<div class="sidebar-note">${escapeHtml(status.skills?.summary ?? "本机暂无已同步 Skill")}</div>
|
||||
<div class="sidebar-kv">
|
||||
<span>同步状态</span>
|
||||
<b class="${syncTone}">${escapeHtml(status.skills?.syncStatus ?? "未同步")}</b>
|
||||
</div>
|
||||
<div class="sidebar-kv">
|
||||
<span>部署数量</span>
|
||||
<b>${escapeHtml(status.skills?.total ?? 0)}</b>
|
||||
</div>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
export function renderBossAgentHtml(status) {
|
||||
return renderBossAgentHtmlBase(status);
|
||||
}
|
||||
@@ -575,44 +519,6 @@ function renderBossAgentHtmlBase(status, options = {}) {
|
||||
font-weight: 800;
|
||||
}
|
||||
.nav a.active .nav-badge { background: rgba(7, 193, 96, .14); color: #058743; }
|
||||
.sidebar-action {
|
||||
display: block;
|
||||
padding: 9px 10px;
|
||||
border-radius: 12px;
|
||||
background: var(--green);
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
font-size: 12px;
|
||||
font-weight: 850;
|
||||
}
|
||||
.sidebar-card {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, .82);
|
||||
box-shadow: 0 8px 24px rgba(21, 35, 27, .04);
|
||||
}
|
||||
.sidebar-title { font-size: 14px; font-weight: 850; letter-spacing: -.02em; }
|
||||
.sidebar-note { color: var(--muted); font-size: 12px; line-height: 1.5; }
|
||||
.sidebar-kv { display: flex; justify-content: space-between; align-items: center; gap: 12px; color: var(--muted); font-size: 12px; }
|
||||
.sidebar-kv b { color: var(--ink); font-size: 13px; }
|
||||
.sidebar-kv b.good { color: #058743; }
|
||||
.sidebar-kv b.warn { color: var(--warn); }
|
||||
.sidebar-kv b.bad { color: var(--bad); }
|
||||
.sidebar-mini-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding-top: 4px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.sidebar-mini-list .permission-row { gap: 10px; align-items: flex-start; }
|
||||
.sidebar-mini-list .permission-name { font-size: 12px; }
|
||||
.sidebar-mini-list .muted { display: none; }
|
||||
.sidebar-mini-list .pill { padding: 4px 8px; font-size: 11px; }
|
||||
.content { padding: 26px 32px 32px; }
|
||||
.topbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 28px; }
|
||||
h1 { margin: 0; font-size: 28px; letter-spacing: -.04em; }
|
||||
@@ -744,17 +650,15 @@ function renderBossAgentHtmlBase(status, options = {}) {
|
||||
</div>
|
||||
</div>
|
||||
<nav class="nav">
|
||||
<a class="active" href="/"><span>概览</span><span class="nav-badge">当前</span></a>
|
||||
<a href="/"><span>本机权限获取</span><span class="nav-badge">${escapeHtml(status.permissionReadiness.coreReady ? "OK" : "待")}</span></a>
|
||||
<a href="/"><span>Skill</span><span class="nav-badge">${escapeHtml(status.skills.total)}</span></a>
|
||||
<a href="/"><span>绑定与授权</span></a>
|
||||
<a href="/"><span>日志</span></a>
|
||||
<a class="active" href="#overview"><span>概览</span><span class="nav-badge">当前</span></a>
|
||||
<a href="#permissions"><span>本机权限获取</span><span class="nav-badge">${escapeHtml(status.permissionReadiness.coreReady ? "OK" : "待")}</span></a>
|
||||
<a href="#skills"><span>Skill</span><span class="nav-badge">${escapeHtml(status.skills.total)}</span></a>
|
||||
<a href="#license"><span>绑定与授权</span></a>
|
||||
<a href="#logs"><span>日志</span></a>
|
||||
</nav>
|
||||
${sidebarPermissionBlock(status)}
|
||||
${sidebarSkillBlock(status)}
|
||||
</aside>
|
||||
<section class="content">
|
||||
<header class="topbar">
|
||||
<header class="topbar" id="overview">
|
||||
<div>
|
||||
<h1>boss-agent</h1>
|
||||
<div class="subtitle">企业电脑接入端</div>
|
||||
@@ -808,11 +712,11 @@ function renderBossAgentHtmlBase(status, options = {}) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card panel setup-panel">
|
||||
<section class="card panel setup-panel" id="permissions">
|
||||
<div class="setup-head">
|
||||
<div>
|
||||
<h2>${escapeHtml(status.permissionSetup.title)}</h2>
|
||||
<p>${escapeHtml(status.permissionSetup.goal)} 当前状态:${escapeHtml(status.permissionSetup.summary)} 后续静默使用依赖系统持久授权。</p>
|
||||
<p>${escapeHtml(status.permissionSetup.goal)} 权限结论:${escapeHtml(status.permissions.summary)}。当前状态:${escapeHtml(status.permissionSetup.summary)} 后续静默使用依赖系统持久授权。</p>
|
||||
</div>
|
||||
<a class="button" href="${escapeHtml(status.permissionSetup.primaryAction.href)}">${escapeHtml(status.permissionSetup.primaryAction.label)}</a>
|
||||
</div>
|
||||
@@ -821,7 +725,7 @@ function renderBossAgentHtmlBase(status, options = {}) {
|
||||
</section>
|
||||
|
||||
<section class="lower">
|
||||
<div class="card panel">
|
||||
<div class="card panel" id="license">
|
||||
<h2>授权信息</h2>
|
||||
<div class="rows">
|
||||
<div class="row"><span class="label">企业</span><span class="value">${escapeHtml(status.license.enterpriseName)}</span></div>
|
||||
@@ -830,7 +734,7 @@ function renderBossAgentHtmlBase(status, options = {}) {
|
||||
</div>
|
||||
<div class="hint">${escapeHtml(status.permissionReadiness.detail)}</div>
|
||||
</div>
|
||||
<div class="card panel">
|
||||
<div class="card panel" id="skills">
|
||||
<h2>Skill 部署情况</h2>
|
||||
<div class="rows">${skillRows(status)}</div>
|
||||
<div class="hint">Skill 用于把本机可复用能力分发给 Boss APP 和主 Agent;后续企业后台可按账号、设备和权限策略下发。</div>
|
||||
|
||||
@@ -20,7 +20,11 @@ mkdir -p "$MACOS_DIR" "$RESOURCES_DIR"
|
||||
swiftc "$SOURCE_FILE" \
|
||||
-o "$BINARY_PATH" \
|
||||
-framework Cocoa \
|
||||
-framework WebKit
|
||||
-framework WebKit \
|
||||
-framework ApplicationServices \
|
||||
-framework AVFoundation \
|
||||
-framework Network \
|
||||
-framework UserNotifications
|
||||
|
||||
chmod +x "$BINARY_PATH"
|
||||
|
||||
@@ -51,6 +55,21 @@ cat > "$CONTENTS_DIR/Info.plist" <<'PLIST'
|
||||
<string>13.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>NSAppleEventsUsageDescription</key>
|
||||
<string>boss-agent 需要通过自动化控制 Finder、浏览器和授权的企业应用,以完成远程桌面级任务。</string>
|
||||
<key>NSScreenCaptureUsageDescription</key>
|
||||
<string>boss-agent 需要读取屏幕画面,用于识别桌面状态、系统弹窗和远程控制结果。</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>boss-agent 需要麦克风权限,用于语音协作和远程指令输入。</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>boss-agent 需要摄像头权限,用于视觉协作和现场画面确认。</string>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>boss-agent 需要访问本地网络,用于发现和连接局域网设备、开发板和企业内网服务。</string>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_http._tcp</string>
|
||||
<string>_boss-agent._tcp</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
PLIST
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
import {
|
||||
buildBossAgentStatus,
|
||||
@@ -122,6 +123,10 @@ test("boss-agent status treats token-backed devices as bound and renders enterpr
|
||||
assert.match(html, /完整接管待补齐/);
|
||||
assert.match(html, /一次完整授权/);
|
||||
assert.match(html, /后续静默使用/);
|
||||
assert.match(html, /href="#permissions"><span>本机权限获取<\/span>/);
|
||||
assert.match(html, /href="#skills"><span>Skill<\/span>/);
|
||||
assert.doesNotMatch(html, /<section class="sidebar-card">/);
|
||||
assert.doesNotMatch(html, /<div class="sidebar-title">本机权限获取<\/div>/);
|
||||
assert.match(html, /api\/v1\/boss-agent\/permissions\/open\?target=all/);
|
||||
assert.match(html, /api\/v1\/boss-agent\/permissions\/open\?target=fullDiskAccess/);
|
||||
assert.match(html, /Skill/);
|
||||
@@ -153,3 +158,22 @@ test("boss-agent unbound HTML renders a real scannable QR image when qrcode is a
|
||||
assert.match(html, /data:image\/png;base64/);
|
||||
assert.match(html, /Boss APP 绑定二维码/);
|
||||
});
|
||||
|
||||
test("boss-agent mac app intercepts permission links and triggers native app permission requests", () => {
|
||||
const swiftSource = readFileSync("apps/boss-agent-mac/Sources/BossAgentApp.swift", "utf8");
|
||||
const buildScript = readFileSync("scripts/build-boss-agent-mac-app.sh", "utf8");
|
||||
|
||||
assert.match(swiftSource, /decidePolicyFor navigationAction/);
|
||||
assert.match(swiftSource, /api\/v1\/boss-agent\/permissions\/open/);
|
||||
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, /CGEvent\.tapCreate/);
|
||||
assert.match(swiftSource, /NSWorkspace\.shared\.open/);
|
||||
assert.match(buildScript, /NSMicrophoneUsageDescription/);
|
||||
assert.match(buildScript, /NSCameraUsageDescription/);
|
||||
assert.match(buildScript, /NSAppleEventsUsageDescription/);
|
||||
assert.match(buildScript, /NSLocalNetworkUsageDescription/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user