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? {
|
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 = [
|
let databasePaths = [
|
||||||
"/Library/Application Support/com.apple.TCC/TCC.db",
|
"/Library/Application Support/com.apple.TCC/TCC.db",
|
||||||
"\(NSHomeDirectory())/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_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 BOSS_AGENT_DEFAULTS_DOMAIN = "com.hyzq.boss.agent";
|
||||||
const TCC_PERMISSION_SERVICES = {
|
const TCC_PERMISSION_SERVICES = {
|
||||||
kTCCServiceAccessibility: "accessibility",
|
kTCCServiceAccessibility: "accessibility",
|
||||||
kTCCServiceScreenCapture: "screenRecording",
|
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 = [
|
const TCC_PERMISSION_DATABASES = [
|
||||||
"/Library/Application Support/com.apple.TCC/TCC.db",
|
"/Library/Application Support/com.apple.TCC/TCC.db",
|
||||||
path.join(os.homedir(), "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)}`;
|
return `${text.slice(0, 4)}••••${text.slice(-4)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sqlQuote(value) {
|
||||||
|
return `'${String(value ?? "").replaceAll("'", "''")}'`;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizePermissionStatus(value) {
|
function normalizePermissionStatus(value) {
|
||||||
return value === "granted" || value === "missing" || value === "unknown" ? value : "unknown";
|
return value === "granted" || value === "missing" || value === "unknown" ? value : "unknown";
|
||||||
}
|
}
|
||||||
@@ -171,7 +189,7 @@ function resolvePermissionReadiness(coreItems, extendedItems) {
|
|||||||
extendedTotal: extendedItems.length,
|
extendedTotal: extendedItems.length,
|
||||||
summary,
|
summary,
|
||||||
detail:
|
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;
|
return merged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mergeBossComputerUseHelperScreenRecordingPermission(permissions = {}, helperStatus = "unknown") {
|
||||||
|
if (helperStatus !== "granted") return { ...permissions };
|
||||||
|
return {
|
||||||
|
...permissions,
|
||||||
|
screenRecording: "granted",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function mergeBossAgentAppTccPermissions(permissions = {}, tccRows = "") {
|
export function mergeBossAgentAppTccPermissions(permissions = {}, tccRows = "") {
|
||||||
const merged = { ...permissions };
|
const merged = { ...permissions };
|
||||||
const granted = new Set();
|
const granted = new Set();
|
||||||
@@ -254,9 +280,14 @@ export function mergeBossAgentAppTccPermissions(permissions = {}, tccRows = "")
|
|||||||
for (const rawLine of String(tccRows ?? "").split(/\r?\n/)) {
|
for (const rawLine of String(tccRows ?? "").split(/\r?\n/)) {
|
||||||
const line = rawLine.trim();
|
const line = rawLine.trim();
|
||||||
if (!line) continue;
|
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];
|
const permissionKey = TCC_PERMISSION_SERVICES[service];
|
||||||
if (!permissionKey) continue;
|
if (!permissionKey) continue;
|
||||||
|
const allowedClients = TCC_PERMISSION_CLIENTS[service] ?? [];
|
||||||
|
if (client && allowedClients.length > 0 && !allowedClients.includes(client)) continue;
|
||||||
if (authValue === "2") {
|
if (authValue === "2") {
|
||||||
merged[permissionKey] = "granted";
|
merged[permissionKey] = "granted";
|
||||||
granted.add(permissionKey);
|
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") {
|
function normalizePermissionTarget(target = "core") {
|
||||||
return Object.hasOwn(MACOS_PERMISSION_SETTINGS, target) ? 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",
|
accessibility: accessibility.ok && /true/i.test(accessibility.stdout) ? "granted" : "missing",
|
||||||
screenRecording,
|
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());
|
return mergeBossAgentStoredNativePermissions(appTccPermissions, await readBossAgentStoredNativePermissions());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -992,11 +1082,13 @@ async function readBossAgentAppTccRows() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const service of Object.keys(TCC_PERMISSION_SERVICES)) {
|
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 = [
|
const query = [
|
||||||
"select service,auth_value from access",
|
"select service,client,auth_value from access",
|
||||||
`where client='${BOSS_AGENT_BUNDLE_ID}'`,
|
`where client in (${clientList})`,
|
||||||
`and service='${service}'`,
|
`and service='${service}'`,
|
||||||
"limit 1",
|
"order by auth_value desc",
|
||||||
].join(" ");
|
].join(" ");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1004,7 +1096,7 @@ async function readBossAgentAppTccRows() {
|
|||||||
try {
|
try {
|
||||||
const rows = db.prepare(query).all();
|
const rows = db.prepare(query).all();
|
||||||
if (rows.length > 0) {
|
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 {
|
} finally {
|
||||||
db.close();
|
db.close();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { readFileSync } from "node:fs";
|
|||||||
import {
|
import {
|
||||||
buildBossAgentStatus,
|
buildBossAgentStatus,
|
||||||
mergeBossAgentAppTccPermissions,
|
mergeBossAgentAppTccPermissions,
|
||||||
|
mergeBossComputerUseHelperScreenRecordingPermission,
|
||||||
mergeBossAgentStoredNativePermissions,
|
mergeBossAgentStoredNativePermissions,
|
||||||
mergeBossAgentNativePermissionOverrides,
|
mergeBossAgentNativePermissionOverrides,
|
||||||
renderBossAgentHtml,
|
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", () => {
|
test("boss-agent permission detection trusts stored native app permission grants", () => {
|
||||||
const merged = mergeBossAgentStoredNativePermissions(
|
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, /CGRequestScreenCaptureAccess/);
|
||||||
assert.match(swiftSource, /tccPermissionStatus/);
|
assert.match(swiftSource, /tccPermissionStatus/);
|
||||||
assert.match(swiftSource, /kTCCServiceScreenCapture/);
|
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.doesNotMatch(swiftSource, /AVCaptureDevice\.requestAccess|UNUserNotificationCenter|import IOKit\.hid|CGRequestListenEventAccess|IOHIDRequestAccess|CGEvent\.tapCreate|NWBrowser/);
|
||||||
assert.match(swiftSource, /NSWorkspace\.shared\.open/);
|
assert.match(swiftSource, /NSWorkspace\.shared\.open/);
|
||||||
assert.match(swiftSource, /deadline: \.now\(\) \+ 0\.45/);
|
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");
|
const statusSource = readFileSync("local-agent/boss-agent-status.mjs", "utf8");
|
||||||
assert.match(statusSource, /boss-agent:\/\/permissions\/open/);
|
assert.match(statusSource, /boss-agent:\/\/permissions\/open/);
|
||||||
assert.match(statusSource, /com\.hyzq\.boss\.agent/);
|
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, /"-na"/);
|
||||||
assert.match(statusSource, /--request-permission/);
|
assert.match(statusSource, /--request-permission/);
|
||||||
assert.match(statusSource, /\/Applications\/boss-agent\.app/);
|
assert.match(statusSource, /\/Applications\/boss-agent\.app/);
|
||||||
|
|||||||
Reference in New Issue
Block a user