fix: detect helper screen recording permission
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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/);
|
||||
|
||||
Reference in New Issue
Block a user