fix: detect helper screen recording permission

This commit is contained in:
AI Bot
2026-05-13 10:35:59 +08:00
parent 04505da747
commit 67511c31f4
3 changed files with 139 additions and 8 deletions

View File

@@ -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",

View File

@@ -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();

View File

@@ -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/);