fix: sync native agent permission states
This commit is contained in:
@@ -14,9 +14,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate {
|
||||
private var webView: WKWebView?
|
||||
private var inputMonitoringTap: CFMachPort?
|
||||
private var localNetworkBrowser: NWBrowser?
|
||||
private var activeTab = "overview"
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
NSApp.setActivationPolicy(.regular)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleApplicationDidBecomeActive),
|
||||
name: NSApplication.didBecomeActiveNotification,
|
||||
object: nil
|
||||
)
|
||||
|
||||
let webConfiguration = WKWebViewConfiguration()
|
||||
let webView = WKWebView(frame: .zero, configuration: webConfiguration)
|
||||
@@ -38,18 +45,29 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate {
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
self.window = window
|
||||
|
||||
loadAgentPanel()
|
||||
loadAgentPanel(tab: activeTab)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
|
||||
private func loadAgentPanel() {
|
||||
guard let url = URL(string: "http://127.0.0.1:4317/boss-agent") else {
|
||||
private func loadAgentPanel(tab: String? = nil) {
|
||||
activeTab = normalizedTab(tab ?? activeTab)
|
||||
var components = URLComponents()
|
||||
components.scheme = "http"
|
||||
components.host = "127.0.0.1"
|
||||
components.port = 4317
|
||||
components.path = "/boss-agent"
|
||||
components.queryItems = [URLQueryItem(name: "tab", value: activeTab)] + nativePermissionQueryItems()
|
||||
guard let url = components.url else {
|
||||
loadFallback()
|
||||
return
|
||||
}
|
||||
webView?.load(URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData))
|
||||
}
|
||||
|
||||
@objc private func handleApplicationDidBecomeActive() {
|
||||
loadAgentPanel(tab: activeTab)
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
|
||||
loadFallback()
|
||||
}
|
||||
@@ -74,6 +92,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
if isAgentPanelUrl(url) && !hasNativePermissionQuery(url) {
|
||||
decisionHandler(.cancel)
|
||||
loadAgentPanel(tab: queryValue("tab", in: url))
|
||||
return
|
||||
}
|
||||
|
||||
if isAgentPanelUrl(url), let tab = queryValue("tab", in: url) {
|
||||
activeTab = normalizedTab(tab)
|
||||
}
|
||||
|
||||
decisionHandler(.allow)
|
||||
}
|
||||
|
||||
@@ -81,12 +109,38 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate {
|
||||
url.path == "/api/v1/boss-agent/permissions/open"
|
||||
}
|
||||
|
||||
private func isAgentPanelUrl(_ url: URL) -> Bool {
|
||||
let host = url.host ?? ""
|
||||
return (host == "127.0.0.1" || host == "localhost") && url.port == 4317 && (url.path == "/boss-agent" || url.path == "/")
|
||||
}
|
||||
|
||||
private func hasNativePermissionQuery(_ url: URL) -> Bool {
|
||||
URLComponents(url: url, resolvingAgainstBaseURL: false)?
|
||||
.queryItems?
|
||||
.contains(where: { $0.name.hasPrefix("native") }) == true
|
||||
}
|
||||
|
||||
private func queryValue(_ name: String, in url: URL) -> String? {
|
||||
URLComponents(url: url, resolvingAgainstBaseURL: false)?
|
||||
.queryItems?
|
||||
.first(where: { $0.name == name })?
|
||||
.value
|
||||
}
|
||||
|
||||
private func normalizedTab(_ value: String?) -> String {
|
||||
switch value {
|
||||
case "permissions", "skills", "license", "logs", "overview":
|
||||
return value ?? "overview"
|
||||
default:
|
||||
return "overview"
|
||||
}
|
||||
}
|
||||
|
||||
private func handlePermissionSetupNavigation(_ url: URL) {
|
||||
let target =
|
||||
URLComponents(url: url, resolvingAgainstBaseURL: false)?
|
||||
.queryItems?
|
||||
.first(where: { $0.name == "target" })?
|
||||
.value ?? "all"
|
||||
queryValue("target", in: url) ?? "all"
|
||||
let returnTab = normalizedTab(queryValue("returnTab", in: url) ?? activeTab)
|
||||
activeTab = returnTab
|
||||
|
||||
requestNativePermission(for: target)
|
||||
if let settingsUrl = systemSettingsUrl(for: target) {
|
||||
@@ -94,7 +148,49 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate {
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { [weak self] in
|
||||
self?.loadAgentPanel()
|
||||
self?.loadAgentPanel(tab: returnTab)
|
||||
}
|
||||
}
|
||||
|
||||
private func nativePermissionQueryItems() -> [URLQueryItem] {
|
||||
[
|
||||
URLQueryItem(name: "nativeAccessibility", value: AXIsProcessTrusted() ? "granted" : "missing"),
|
||||
URLQueryItem(name: "nativeScreenRecording", value: screenRecordingStatus()),
|
||||
URLQueryItem(name: "nativeMicrophone", value: microphonePermissionStatus()),
|
||||
URLQueryItem(name: "nativeCamera", value: cameraPermissionStatus()),
|
||||
]
|
||||
}
|
||||
|
||||
private func screenRecordingStatus() -> String {
|
||||
if #available(macOS 10.15, *) {
|
||||
return CGPreflightScreenCaptureAccess() ? "granted" : "missing"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
private func microphonePermissionStatus() -> String {
|
||||
switch AVCaptureDevice.authorizationStatus(for: .audio) {
|
||||
case .authorized:
|
||||
return "granted"
|
||||
case .denied, .restricted:
|
||||
return "missing"
|
||||
case .notDetermined:
|
||||
return "unknown"
|
||||
@unknown default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
private func cameraPermissionStatus() -> String {
|
||||
switch AVCaptureDevice.authorizationStatus(for: .video) {
|
||||
case .authorized:
|
||||
return "granted"
|
||||
case .denied, .restricted:
|
||||
return "missing"
|
||||
case .notDetermined:
|
||||
return "unknown"
|
||||
@unknown default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,9 +229,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate {
|
||||
case "notifications":
|
||||
requestNotificationPermission()
|
||||
case "microphone":
|
||||
AVCaptureDevice.requestAccess(for: .audio) { _ in }
|
||||
requestMicrophonePermission()
|
||||
case "camera":
|
||||
AVCaptureDevice.requestAccess(for: .video) { _ in }
|
||||
requestCameraPermission()
|
||||
case "localNetwork":
|
||||
requestLocalNetworkPermission()
|
||||
default:
|
||||
@@ -149,6 +245,18 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate {
|
||||
_ = AXIsProcessTrustedWithOptions(options)
|
||||
}
|
||||
|
||||
private func requestMicrophonePermission() {
|
||||
if AVCaptureDevice.authorizationStatus(for: .audio) == .notDetermined {
|
||||
AVCaptureDevice.requestAccess(for: .audio) { _ in }
|
||||
}
|
||||
}
|
||||
|
||||
private func requestCameraPermission() {
|
||||
if AVCaptureDevice.authorizationStatus(for: .video) == .notDetermined {
|
||||
AVCaptureDevice.requestAccess(for: .video) { _ in }
|
||||
}
|
||||
}
|
||||
|
||||
private func requestScreenRecordingPermission() {
|
||||
if #available(macOS 10.15, *) {
|
||||
_ = CGRequestScreenCaptureAccess()
|
||||
|
||||
@@ -78,6 +78,13 @@ const MACOS_PERMISSION_SETTINGS = {
|
||||
|
||||
const AUTO_PREFLIGHT_PERMISSION_KEYS = new Set(["accessibility", "screenRecording", "automation"]);
|
||||
|
||||
const NATIVE_PERMISSION_QUERY_PARAMS = {
|
||||
accessibility: "nativeAccessibility",
|
||||
screenRecording: "nativeScreenRecording",
|
||||
microphone: "nativeMicrophone",
|
||||
camera: "nativeCamera",
|
||||
};
|
||||
|
||||
function nonEmpty(value) {
|
||||
const text = String(value ?? "").trim();
|
||||
return text || undefined;
|
||||
@@ -103,6 +110,10 @@ function normalizePermissionStatus(value) {
|
||||
return value === "granted" || value === "missing" || value === "unknown" ? value : "unknown";
|
||||
}
|
||||
|
||||
function isPermissionStatus(value) {
|
||||
return value === "granted" || value === "missing" || value === "unknown";
|
||||
}
|
||||
|
||||
function statusTone(status) {
|
||||
if (status === "granted" || status === "valid" || status === "connected" || status === "bound") {
|
||||
return "good";
|
||||
@@ -220,7 +231,7 @@ function buildPermissionSetupPlan(coreItems, extendedItems, readiness) {
|
||||
requiredForSilentControl: true,
|
||||
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)}`,
|
||||
openUrl: `/api/v1/boss-agent/permissions/open?target=${encodeURIComponent(item.key)}&returnTab=permissions`,
|
||||
owner: "boss-agent.app",
|
||||
}));
|
||||
const missingActions = actions.filter((action) => action.status !== "granted");
|
||||
@@ -232,7 +243,7 @@ function buildPermissionSetupPlan(coreItems, extendedItems, readiness) {
|
||||
silentUseReady: missingActions.length === 0,
|
||||
primaryAction: {
|
||||
label: "打开完整授权向导",
|
||||
href: "/api/v1/boss-agent/permissions/open?target=all",
|
||||
href: "/api/v1/boss-agent/permissions/open?target=all&returnTab=permissions",
|
||||
settingsUrl: MACOS_PERMISSION_SETTINGS.all,
|
||||
},
|
||||
actions,
|
||||
@@ -245,6 +256,21 @@ function buildPermissionSetupPlan(coreItems, extendedItems, readiness) {
|
||||
};
|
||||
}
|
||||
|
||||
export function mergeBossAgentNativePermissionOverrides(permissions = {}, queryParams = {}) {
|
||||
const getQueryValue = (name) => {
|
||||
if (typeof queryParams.get === "function") return queryParams.get(name);
|
||||
return queryParams[name];
|
||||
};
|
||||
const merged = { ...permissions };
|
||||
for (const [permissionKey, queryKey] of Object.entries(NATIVE_PERMISSION_QUERY_PARAMS)) {
|
||||
const value = getQueryValue(queryKey);
|
||||
if (isPermissionStatus(value)) {
|
||||
merged[permissionKey] = value;
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function resolveSkills(runtime) {
|
||||
const rawSkills = Array.isArray(runtime.lastSkills) ? runtime.lastSkills : [];
|
||||
const items = rawSkills
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
import {
|
||||
buildBossAgentStatus,
|
||||
detectLocalComputerPermissions,
|
||||
mergeBossAgentNativePermissionOverrides,
|
||||
normalizeBossAgentTab,
|
||||
openBossAgentPermissionSettings,
|
||||
renderBossAgentHtmlWithQr,
|
||||
@@ -1010,7 +1011,10 @@ const server = createServer(async (request, response) => {
|
||||
const requestUrl = new URL(request.url || "/", `http://${config.bindHost || "127.0.0.1"}`);
|
||||
|
||||
if (requestUrl.pathname === "/" || requestUrl.pathname === "/boss-agent") {
|
||||
const permissions = await detectLocalComputerPermissions();
|
||||
const permissions = mergeBossAgentNativePermissionOverrides(
|
||||
await detectLocalComputerPermissions(),
|
||||
requestUrl.searchParams,
|
||||
);
|
||||
const status = buildBossAgentStatus(config, runtime, { permissions });
|
||||
const activeTab = normalizeBossAgentTab(requestUrl.searchParams.get("tab") ?? "overview");
|
||||
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
||||
@@ -1034,6 +1038,7 @@ const server = createServer(async (request, response) => {
|
||||
|
||||
if (requestUrl.pathname === "/api/v1/boss-agent/permissions/open") {
|
||||
const target = requestUrl.searchParams.get("target") || "all";
|
||||
const returnTab = normalizeBossAgentTab(requestUrl.searchParams.get("returnTab") ?? "permissions");
|
||||
const result = await openBossAgentPermissionSettings(target);
|
||||
const wantsJson = String(request.headers.accept || "").includes("application/json");
|
||||
if (wantsJson) {
|
||||
@@ -1041,7 +1046,7 @@ const server = createServer(async (request, response) => {
|
||||
response.end(JSON.stringify(result));
|
||||
return;
|
||||
}
|
||||
response.writeHead(302, { Location: "/boss-agent" });
|
||||
response.writeHead(302, { Location: `/boss-agent?tab=${encodeURIComponent(returnTab)}` });
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { readFileSync } from "node:fs";
|
||||
|
||||
import {
|
||||
buildBossAgentStatus,
|
||||
mergeBossAgentNativePermissionOverrides,
|
||||
renderBossAgentHtml,
|
||||
renderBossAgentHtmlWithQr,
|
||||
} from "../local-agent/boss-agent-status.mjs";
|
||||
@@ -169,8 +170,8 @@ test("boss-agent permission and skill menu entries render as separate tab pages"
|
||||
assert.match(permissionsHtml, /<h2>一次完整授权<\/h2>/);
|
||||
assert.match(permissionsHtml, /完整接管待补齐/);
|
||||
assert.match(permissionsHtml, /后续静默使用/);
|
||||
assert.match(permissionsHtml, /api\/v1\/boss-agent\/permissions\/open\?target=all/);
|
||||
assert.match(permissionsHtml, /api\/v1\/boss-agent\/permissions\/open\?target=fullDiskAccess/);
|
||||
assert.match(permissionsHtml, /api\/v1\/boss-agent\/permissions\/open\?target=all&returnTab=permissions/);
|
||||
assert.match(permissionsHtml, /api\/v1\/boss-agent\/permissions\/open\?target=fullDiskAccess&returnTab=permissions/);
|
||||
assert.doesNotMatch(permissionsHtml, /<h2>Skill 部署情况<\/h2>/);
|
||||
|
||||
const skillsHtml = renderBossAgentHtml(status, { activeTab: "skills" });
|
||||
@@ -180,6 +181,33 @@ test("boss-agent permission and skill menu entries render as separate tab pages"
|
||||
assert.doesNotMatch(skillsHtml, /<h2>一次完整授权<\/h2>/);
|
||||
});
|
||||
|
||||
test("boss-agent native permission overrides update app-owned camera and microphone state", () => {
|
||||
const merged = mergeBossAgentNativePermissionOverrides(
|
||||
{
|
||||
accessibility: "missing",
|
||||
screenRecording: "missing",
|
||||
automation: "granted",
|
||||
camera: "unknown",
|
||||
microphone: "unknown",
|
||||
},
|
||||
new URLSearchParams({
|
||||
nativeAccessibility: "granted",
|
||||
nativeCamera: "granted",
|
||||
nativeMicrophone: "missing",
|
||||
nativeScreenRecording: "granted",
|
||||
nativeInputMonitoring: "invalid",
|
||||
}),
|
||||
);
|
||||
|
||||
assert.deepEqual(merged, {
|
||||
accessibility: "granted",
|
||||
screenRecording: "granted",
|
||||
automation: "granted",
|
||||
camera: "granted",
|
||||
microphone: "missing",
|
||||
});
|
||||
});
|
||||
|
||||
test("boss-agent unbound HTML renders a real scannable QR image when qrcode is available", async () => {
|
||||
const status = buildBossAgentStatus(
|
||||
{
|
||||
@@ -215,6 +243,14 @@ test("boss-agent mac app intercepts permission links and triggers native app per
|
||||
assert.match(swiftSource, /UNUserNotificationCenter\.current\(\)\.requestAuthorization/);
|
||||
assert.match(swiftSource, /CGEvent\.tapCreate/);
|
||||
assert.match(swiftSource, /NSWorkspace\.shared\.open/);
|
||||
assert.match(swiftSource, /loadAgentPanel\(tab:/);
|
||||
assert.match(swiftSource, /returnTab/);
|
||||
assert.match(swiftSource, /nativePermissionQueryItems/);
|
||||
assert.match(swiftSource, /nativeCamera/);
|
||||
assert.match(swiftSource, /nativeMicrophone/);
|
||||
assert.match(swiftSource, /AVCaptureDevice\.authorizationStatus\(for: \.video/);
|
||||
assert.match(swiftSource, /AVCaptureDevice\.authorizationStatus\(for: \.audio/);
|
||||
assert.match(swiftSource, /NSApplication\.didBecomeActiveNotification/);
|
||||
assert.match(buildScript, /NSMicrophoneUsageDescription/);
|
||||
assert.match(buildScript, /NSCameraUsageDescription/);
|
||||
assert.match(buildScript, /NSAppleEventsUsageDescription/);
|
||||
|
||||
Reference in New Issue
Block a user