fix: reconcile boss agent screen permission state
This commit is contained in:
@@ -221,19 +221,65 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate {
|
||||
}
|
||||
|
||||
private func nativePermissionQueryItems() -> [URLQueryItem] {
|
||||
[
|
||||
URLQueryItem(name: "nativeAccessibility", value: AXIsProcessTrusted() ? "granted" : "missing"),
|
||||
URLQueryItem(name: "nativeScreenRecording", value: screenRecordingStatus()),
|
||||
let accessibility = AXIsProcessTrusted() ? "granted" : "missing"
|
||||
let screenRecording = screenRecordingStatus()
|
||||
UserDefaults.standard.set(accessibility, forKey: "native.accessibility")
|
||||
UserDefaults.standard.set(screenRecording, forKey: "native.screenRecording")
|
||||
|
||||
return [
|
||||
URLQueryItem(name: "nativeAccessibility", value: accessibility),
|
||||
URLQueryItem(name: "nativeScreenRecording", value: screenRecording),
|
||||
]
|
||||
}
|
||||
|
||||
private func screenRecordingStatus() -> String {
|
||||
if #available(macOS 10.15, *) {
|
||||
return CGPreflightScreenCaptureAccess() ? "granted" : "missing"
|
||||
if CGPreflightScreenCaptureAccess() {
|
||||
return "granted"
|
||||
}
|
||||
if CGRequestScreenCaptureAccess() {
|
||||
return "granted"
|
||||
}
|
||||
return tccPermissionStatus(service: "kTCCServiceScreenCapture") ?? "missing"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
private func tccPermissionStatus(service: String) -> String? {
|
||||
let query = "select auth_value from access where client='com.hyzq.boss.agent' and service='\(service)' limit 1;"
|
||||
let databasePaths = [
|
||||
"/Library/Application Support/com.apple.TCC/TCC.db",
|
||||
"\(NSHomeDirectory())/Library/Application Support/com.apple.TCC/TCC.db",
|
||||
]
|
||||
|
||||
for databasePath in databasePaths where FileManager.default.fileExists(atPath: databasePath) {
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/sqlite3")
|
||||
process.arguments = [databasePath, query]
|
||||
let output = Pipe()
|
||||
process.standardOutput = output
|
||||
process.standardError = Pipe()
|
||||
|
||||
do {
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
|
||||
let data = output.fileHandleForReading.readDataToEndOfFile()
|
||||
let value = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if value == "2" {
|
||||
return "granted"
|
||||
}
|
||||
if value == "0" {
|
||||
return "missing"
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private func requestNativePermission(for target: String) {
|
||||
let targets: [String]
|
||||
if target == "core" {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
|
||||
import { rm, stat } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
|
||||
const PERMISSION_DEFS = [
|
||||
{
|
||||
@@ -31,6 +32,17 @@ const NATIVE_PERMISSION_QUERY_PARAMS = {
|
||||
screenRecording: "nativeScreenRecording",
|
||||
};
|
||||
|
||||
const BOSS_AGENT_BUNDLE_ID = "com.hyzq.boss.agent";
|
||||
const BOSS_AGENT_DEFAULTS_DOMAIN = "com.hyzq.boss.agent";
|
||||
const TCC_PERMISSION_SERVICES = {
|
||||
kTCCServiceAccessibility: "accessibility",
|
||||
kTCCServiceScreenCapture: "screenRecording",
|
||||
};
|
||||
const TCC_PERMISSION_DATABASES = [
|
||||
"/Library/Application Support/com.apple.TCC/TCC.db",
|
||||
path.join(os.homedir(), "Library/Application Support/com.apple.TCC/TCC.db"),
|
||||
];
|
||||
|
||||
function nonEmpty(value) {
|
||||
const text = String(value ?? "").trim();
|
||||
return text || undefined;
|
||||
@@ -216,12 +228,46 @@ export function mergeBossAgentNativePermissionOverrides(permissions = {}, queryP
|
||||
for (const [permissionKey, queryKey] of Object.entries(NATIVE_PERMISSION_QUERY_PARAMS)) {
|
||||
const value = getQueryValue(queryKey);
|
||||
if (isPermissionStatus(value)) {
|
||||
if (merged[permissionKey] === "granted" && value !== "granted") {
|
||||
continue;
|
||||
}
|
||||
merged[permissionKey] = value;
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
export function mergeBossAgentStoredNativePermissions(permissions = {}, storedPermissions = {}) {
|
||||
const merged = { ...permissions };
|
||||
for (const permissionKey of Object.keys(NATIVE_PERMISSION_QUERY_PARAMS)) {
|
||||
if (storedPermissions[permissionKey] === "granted") {
|
||||
merged[permissionKey] = "granted";
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
export function mergeBossAgentAppTccPermissions(permissions = {}, tccRows = "") {
|
||||
const merged = { ...permissions };
|
||||
const granted = new Set();
|
||||
|
||||
for (const rawLine of String(tccRows ?? "").split(/\r?\n/)) {
|
||||
const line = rawLine.trim();
|
||||
if (!line) continue;
|
||||
const [service, authValue] = line.split("|");
|
||||
const permissionKey = TCC_PERMISSION_SERVICES[service];
|
||||
if (!permissionKey) continue;
|
||||
if (authValue === "2") {
|
||||
merged[permissionKey] = "granted";
|
||||
granted.add(permissionKey);
|
||||
} else if (!granted.has(permissionKey) && authValue === "0") {
|
||||
merged[permissionKey] = "missing";
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
function resolveSkills(runtime) {
|
||||
const rawSkills = Array.isArray(runtime.lastSkills) ? runtime.lastSkills : [];
|
||||
const items = rawSkills
|
||||
@@ -927,8 +973,66 @@ export async function detectLocalComputerPermissions(platform = process.platform
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
const localProcessPermissions = {
|
||||
accessibility: accessibility.ok && /true/i.test(accessibility.stdout) ? "granted" : "missing",
|
||||
screenRecording,
|
||||
};
|
||||
const appTccPermissions = mergeBossAgentAppTccPermissions(localProcessPermissions, await readBossAgentAppTccRows());
|
||||
return mergeBossAgentStoredNativePermissions(appTccPermissions, await readBossAgentStoredNativePermissions());
|
||||
}
|
||||
|
||||
async function readBossAgentAppTccRows() {
|
||||
const outputs = [];
|
||||
|
||||
for (const dbPath of TCC_PERMISSION_DATABASES) {
|
||||
try {
|
||||
await stat(dbPath);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const service of Object.keys(TCC_PERMISSION_SERVICES)) {
|
||||
const query = [
|
||||
"select service,auth_value from access",
|
||||
`where client='${BOSS_AGENT_BUNDLE_ID}'`,
|
||||
`and service='${service}'`,
|
||||
"limit 1",
|
||||
].join(" ");
|
||||
|
||||
try {
|
||||
const db = new DatabaseSync(dbPath, { readonly: true });
|
||||
try {
|
||||
const rows = db.prepare(query).all();
|
||||
if (rows.length > 0) {
|
||||
outputs.push(rows.map((row) => `${row.service}|${row.auth_value}`).join("\n"));
|
||||
}
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
} catch {
|
||||
// Keep the command-line reader below as a second source of truth.
|
||||
}
|
||||
|
||||
const result = await runCommand("/usr/bin/sqlite3", [dbPath, `${query};`], 2500);
|
||||
if (result.ok && result.stdout) {
|
||||
outputs.push(result.stdout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return outputs.join("\n");
|
||||
}
|
||||
|
||||
async function readBossAgentStoredNativePermissions() {
|
||||
const entries = await Promise.all(
|
||||
Object.keys(NATIVE_PERMISSION_QUERY_PARAMS).map(async (permissionKey) => {
|
||||
const result = await runCommand(
|
||||
"/usr/bin/defaults",
|
||||
["read", BOSS_AGENT_DEFAULTS_DOMAIN, `native.${permissionKey}`],
|
||||
1500,
|
||||
);
|
||||
return [permissionKey, result.ok ? normalizePermissionStatus(result.stdout) : "unknown"];
|
||||
}),
|
||||
);
|
||||
return Object.fromEntries(entries);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { readFileSync } from "node:fs";
|
||||
|
||||
import {
|
||||
buildBossAgentStatus,
|
||||
mergeBossAgentAppTccPermissions,
|
||||
mergeBossAgentStoredNativePermissions,
|
||||
mergeBossAgentNativePermissionOverrides,
|
||||
renderBossAgentHtml,
|
||||
renderBossAgentHtmlWithQr,
|
||||
@@ -254,6 +256,57 @@ test("boss-agent native permission overrides only update core desktop-control pe
|
||||
});
|
||||
});
|
||||
|
||||
test("boss-agent native missing values do not downgrade confirmed app grants", () => {
|
||||
const merged = mergeBossAgentNativePermissionOverrides(
|
||||
{
|
||||
accessibility: "granted",
|
||||
screenRecording: "granted",
|
||||
},
|
||||
new URLSearchParams({
|
||||
nativeAccessibility: "missing",
|
||||
nativeScreenRecording: "missing",
|
||||
}),
|
||||
);
|
||||
|
||||
assert.deepEqual(merged, {
|
||||
accessibility: "granted",
|
||||
screenRecording: "granted",
|
||||
});
|
||||
});
|
||||
|
||||
test("boss-agent permission detection trusts app bundle TCC grants over node process checks", () => {
|
||||
const merged = mergeBossAgentAppTccPermissions(
|
||||
{
|
||||
accessibility: "missing",
|
||||
screenRecording: "missing",
|
||||
},
|
||||
"kTCCServiceAccessibility|2\nkTCCServiceScreenCapture|2",
|
||||
);
|
||||
|
||||
assert.deepEqual(merged, {
|
||||
accessibility: "granted",
|
||||
screenRecording: "granted",
|
||||
});
|
||||
});
|
||||
|
||||
test("boss-agent permission detection trusts stored native app permission grants", () => {
|
||||
const merged = mergeBossAgentStoredNativePermissions(
|
||||
{
|
||||
accessibility: "missing",
|
||||
screenRecording: "missing",
|
||||
},
|
||||
{
|
||||
accessibility: "granted",
|
||||
screenRecording: "granted",
|
||||
},
|
||||
);
|
||||
|
||||
assert.deepEqual(merged, {
|
||||
accessibility: "granted",
|
||||
screenRecording: "granted",
|
||||
});
|
||||
});
|
||||
|
||||
test("boss-agent unbound HTML renders a real scannable QR image when qrcode is available", async () => {
|
||||
const status = buildBossAgentStatus(
|
||||
{
|
||||
@@ -292,6 +345,8 @@ test("boss-agent mac app intercepts permission links and triggers native app per
|
||||
assert.match(swiftSource, /lastPermissionRequestTarget/);
|
||||
assert.match(swiftSource, /AXIsProcessTrustedWithOptions/);
|
||||
assert.match(swiftSource, /CGRequestScreenCaptureAccess/);
|
||||
assert.match(swiftSource, /tccPermissionStatus/);
|
||||
assert.match(swiftSource, /kTCCServiceScreenCapture/);
|
||||
assert.doesNotMatch(swiftSource, /AVCaptureDevice\.requestAccess|UNUserNotificationCenter|import IOKit\.hid|CGRequestListenEventAccess|IOHIDRequestAccess|CGEvent\.tapCreate|NWBrowser/);
|
||||
assert.match(swiftSource, /NSWorkspace\.shared\.open/);
|
||||
assert.match(swiftSource, /deadline: \.now\(\) \+ 0\.45/);
|
||||
|
||||
Reference in New Issue
Block a user