diff --git a/apps/boss-agent-mac/Sources/BossAgentApp.swift b/apps/boss-agent-mac/Sources/BossAgentApp.swift index 9e04da1..34ab80a 100644 --- a/apps/boss-agent-mac/Sources/BossAgentApp.swift +++ b/apps/boss-agent-mac/Sources/BossAgentApp.swift @@ -246,7 +246,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate { } 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 clients = service == "kTCCServiceScreenCapture" + ? "'com.hyzq.boss.agent','site.hyzq.boss.computer-use-helper'" + : "'com.hyzq.boss.agent'" + let query = "select auth_value from access where client in (\(clients)) and service='\(service)' order by auth_value desc limit 1;" let databasePaths = [ "/Library/Application Support/com.apple.TCC/TCC.db", "\(NSHomeDirectory())/Library/Application Support/com.apple.TCC/TCC.db", diff --git a/local-agent/boss-agent-status.mjs b/local-agent/boss-agent-status.mjs index 7984d7f..0b7901f 100644 --- a/local-agent/boss-agent-status.mjs +++ b/local-agent/boss-agent-status.mjs @@ -33,11 +33,25 @@ const NATIVE_PERMISSION_QUERY_PARAMS = { }; const BOSS_AGENT_BUNDLE_ID = "com.hyzq.boss.agent"; +const BOSS_COMPUTER_USE_HELPER_BUNDLE_ID = "site.hyzq.boss.computer-use-helper"; +const BOSS_COMPUTER_USE_HELPER_APP_CANDIDATES = [ + path.join(os.homedir(), "Applications/BossComputerUseHelper.app"), + "/Applications/BossComputerUseHelper.app", +]; +const HELPER_SCREEN_RECORDING_CACHE_TTL_MS = 30_000; +let helperScreenRecordingCache = { + status: "unknown", + expiresAt: 0, +}; const BOSS_AGENT_DEFAULTS_DOMAIN = "com.hyzq.boss.agent"; const TCC_PERMISSION_SERVICES = { kTCCServiceAccessibility: "accessibility", kTCCServiceScreenCapture: "screenRecording", }; +const TCC_PERMISSION_CLIENTS = { + kTCCServiceAccessibility: [BOSS_AGENT_BUNDLE_ID], + kTCCServiceScreenCapture: [BOSS_AGENT_BUNDLE_ID, BOSS_COMPUTER_USE_HELPER_BUNDLE_ID], +}; const TCC_PERMISSION_DATABASES = [ "/Library/Application Support/com.apple.TCC/TCC.db", path.join(os.homedir(), "Library/Application Support/com.apple.TCC/TCC.db"), @@ -64,6 +78,10 @@ function maskValue(value) { return `${text.slice(0, 4)}••••${text.slice(-4)}`; } +function sqlQuote(value) { + return `'${String(value ?? "").replaceAll("'", "''")}'`; +} + function normalizePermissionStatus(value) { return value === "granted" || value === "missing" || value === "unknown" ? value : "unknown"; } @@ -171,7 +189,7 @@ function resolvePermissionReadiness(coreItems, extendedItems) { extendedTotal: extendedItems.length, summary, detail: - "参考 Codex Computer Use 的最小权限模型,boss-agent 只要求辅助功能和屏幕录制:辅助功能负责点击输入,屏幕录制负责画面识别。", + "参考 Codex Computer Use 的最小权限模型,boss-agent 只要求辅助功能和屏幕录制:辅助功能负责点击输入,屏幕录制可由 Boss Computer Use Helper 提供画面识别。", }; } @@ -247,6 +265,14 @@ export function mergeBossAgentStoredNativePermissions(permissions = {}, storedPe return merged; } +export function mergeBossComputerUseHelperScreenRecordingPermission(permissions = {}, helperStatus = "unknown") { + if (helperStatus !== "granted") return { ...permissions }; + return { + ...permissions, + screenRecording: "granted", + }; +} + export function mergeBossAgentAppTccPermissions(permissions = {}, tccRows = "") { const merged = { ...permissions }; const granted = new Set(); @@ -254,9 +280,14 @@ export function mergeBossAgentAppTccPermissions(permissions = {}, tccRows = "") for (const rawLine of String(tccRows ?? "").split(/\r?\n/)) { const line = rawLine.trim(); if (!line) continue; - const [service, authValue] = line.split("|"); + const parts = line.split("|"); + const [service] = parts; + const authValue = parts.length >= 3 ? parts[2] : parts[1]; + const client = parts.length >= 3 ? parts[1] : BOSS_AGENT_BUNDLE_ID; const permissionKey = TCC_PERMISSION_SERVICES[service]; if (!permissionKey) continue; + const allowedClients = TCC_PERMISSION_CLIENTS[service] ?? []; + if (client && allowedClients.length > 0 && !allowedClients.includes(client)) continue; if (authValue === "2") { merged[permissionKey] = "granted"; granted.add(permissionKey); @@ -871,6 +902,61 @@ function runCommand(command, args, timeoutMs = 2500) { }); } +async function findBossComputerUseHelperApp() { + for (const appPath of BOSS_COMPUTER_USE_HELPER_APP_CANDIDATES) { + try { + const info = await stat(appPath); + if (info.isDirectory()) return appPath; + } catch { + // Try the next installation location. + } + } + return ""; +} + +async function detectBossComputerUseHelperScreenRecording() { + const now = Date.now(); + if (helperScreenRecordingCache.expiresAt > now) { + return helperScreenRecordingCache.status; + } + + const helperApp = await findBossComputerUseHelperApp(); + if (!helperApp) { + helperScreenRecordingCache = { + status: "unknown", + expiresAt: now + HELPER_SCREEN_RECORDING_CACHE_TTL_MS, + }; + return helperScreenRecordingCache.status; + } + + const screenshotPath = path.join(os.tmpdir(), `boss-helper-screen-permission-${now}.png`); + try { + const result = await runCommand( + "open", + ["-W", "-na", helperApp, "--args", "screenshot", "--path", screenshotPath], + 5000, + ); + if (result.ok) { + const info = await stat(screenshotPath).catch(() => null); + if (info?.size > 1024) { + helperScreenRecordingCache = { + status: "granted", + expiresAt: now + HELPER_SCREEN_RECORDING_CACHE_TTL_MS, + }; + return helperScreenRecordingCache.status; + } + } + } finally { + await rm(screenshotPath, { force: true }).catch(() => {}); + } + + helperScreenRecordingCache = { + status: "missing", + expiresAt: now + HELPER_SCREEN_RECORDING_CACHE_TTL_MS, + }; + return helperScreenRecordingCache.status; +} + function normalizePermissionTarget(target = "core") { return Object.hasOwn(MACOS_PERMISSION_SETTINGS, target) ? target : "core"; } @@ -977,7 +1063,11 @@ export async function detectLocalComputerPermissions(platform = process.platform accessibility: accessibility.ok && /true/i.test(accessibility.stdout) ? "granted" : "missing", screenRecording, }; - const appTccPermissions = mergeBossAgentAppTccPermissions(localProcessPermissions, await readBossAgentAppTccRows()); + const helperPermissions = mergeBossComputerUseHelperScreenRecordingPermission( + localProcessPermissions, + screenRecording === "granted" ? "unknown" : await detectBossComputerUseHelperScreenRecording(), + ); + const appTccPermissions = mergeBossAgentAppTccPermissions(helperPermissions, await readBossAgentAppTccRows()); return mergeBossAgentStoredNativePermissions(appTccPermissions, await readBossAgentStoredNativePermissions()); } @@ -992,11 +1082,13 @@ async function readBossAgentAppTccRows() { } for (const service of Object.keys(TCC_PERMISSION_SERVICES)) { + const clients = TCC_PERMISSION_CLIENTS[service] ?? [BOSS_AGENT_BUNDLE_ID]; + const clientList = clients.map(sqlQuote).join(","); const query = [ - "select service,auth_value from access", - `where client='${BOSS_AGENT_BUNDLE_ID}'`, + "select service,client,auth_value from access", + `where client in (${clientList})`, `and service='${service}'`, - "limit 1", + "order by auth_value desc", ].join(" "); try { @@ -1004,7 +1096,7 @@ async function readBossAgentAppTccRows() { try { const rows = db.prepare(query).all(); if (rows.length > 0) { - outputs.push(rows.map((row) => `${row.service}|${row.auth_value}`).join("\n")); + outputs.push(rows.map((row) => `${row.service}|${row.client}|${row.auth_value}`).join("\n")); } } finally { db.close(); diff --git a/tests/boss-agent-status.test.mjs b/tests/boss-agent-status.test.mjs index 321ba3c..6badf58 100644 --- a/tests/boss-agent-status.test.mjs +++ b/tests/boss-agent-status.test.mjs @@ -5,6 +5,7 @@ import { readFileSync } from "node:fs"; import { buildBossAgentStatus, mergeBossAgentAppTccPermissions, + mergeBossComputerUseHelperScreenRecordingPermission, mergeBossAgentStoredNativePermissions, mergeBossAgentNativePermissionOverrides, renderBossAgentHtml, @@ -289,6 +290,36 @@ test("boss-agent permission detection trusts app bundle TCC grants over node pro }); }); +test("boss-agent permission detection accepts computer-use helper screen capture grants", () => { + const merged = mergeBossAgentAppTccPermissions( + { + accessibility: "granted", + screenRecording: "missing", + }, + "kTCCServiceScreenCapture|site.hyzq.boss.computer-use-helper|2", + ); + + assert.deepEqual(merged, { + accessibility: "granted", + screenRecording: "granted", + }); +}); + +test("boss-agent permission detection promotes successful computer-use helper capture", () => { + const merged = mergeBossComputerUseHelperScreenRecordingPermission( + { + accessibility: "granted", + screenRecording: "missing", + }, + "granted", + ); + + assert.deepEqual(merged, { + accessibility: "granted", + screenRecording: "granted", + }); +}); + test("boss-agent permission detection trusts stored native app permission grants", () => { const merged = mergeBossAgentStoredNativePermissions( { @@ -347,6 +378,7 @@ test("boss-agent mac app intercepts permission links and triggers native app per assert.match(swiftSource, /CGRequestScreenCaptureAccess/); assert.match(swiftSource, /tccPermissionStatus/); assert.match(swiftSource, /kTCCServiceScreenCapture/); + assert.match(swiftSource, /site\.hyzq\.boss\.computer-use-helper/); 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/); @@ -372,6 +404,10 @@ test("boss-agent mac app intercepts permission links and triggers native app per 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, /site\.hyzq\.boss\.computer-use-helper/); + assert.match(statusSource, /BossComputerUseHelper\.app/); + assert.match(statusSource, /screenshot/); + assert.match(statusSource, /--path/); assert.match(statusSource, /"-na"/); assert.match(statusSource, /--request-permission/); assert.match(statusSource, /\/Applications\/boss-agent\.app/);