diff --git a/apps/boss-agent-mac/Sources/BossAgentApp.swift b/apps/boss-agent-mac/Sources/BossAgentApp.swift index 2d809fd..752fa97 100644 --- a/apps/boss-agent-mac/Sources/BossAgentApp.swift +++ b/apps/boss-agent-mac/Sources/BossAgentApp.swift @@ -290,8 +290,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate { } private func requestNativePermission(for target: String) { - let targets = target == "all" - ? [ + let targets: [String] + if target == "core" { + targets = ["accessibility", "screenRecording"] + } else if target == "all" { + targets = [ "accessibility", "screenRecording", "automation", @@ -302,7 +305,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate { "camera", "localNetwork", ] - : [target] + } else { + targets = [target] + } for permission in targets { requestSingleNativePermission(permission) @@ -447,6 +452,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate { private func systemSettingsUrl(for target: String) -> URL? { let mapping = [ "all": "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Security", + "core": "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Accessibility", "accessibility": "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Accessibility", "screenRecording": "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_ScreenCapture", "automation": "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Automation", diff --git a/local-agent/boss-agent-status.mjs b/local-agent/boss-agent-status.mjs index 9769ade..29748ef 100644 --- a/local-agent/boss-agent-status.mjs +++ b/local-agent/boss-agent-status.mjs @@ -16,15 +16,15 @@ const PERMISSION_DEFS = [ description: "用于识别桌面画面和系统弹窗", tier: "core", }, - { - key: "automation", - label: "自动化控制", - description: "用于控制 Finder、浏览器和企业软件", - tier: "core", - }, ]; const EXTENDED_PERMISSION_DEFS = [ + { + key: "automation", + label: "自动化控制", + description: "用于 AppleScript 控制 Finder、浏览器和企业软件;基础桌面控制不强制依赖", + tier: "extended", + }, { key: "fullDiskAccess", label: "全磁盘访问", @@ -65,6 +65,7 @@ const EXTENDED_PERMISSION_DEFS = [ const MACOS_PERMISSION_SETTINGS = { all: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Security", + core: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Accessibility", accessibility: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Accessibility", screenRecording: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_ScreenCapture", automation: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Automation", @@ -204,10 +205,10 @@ function resolvePermissionReadiness(coreItems, extendedItems) { const coreReady = coreGrantedCount === coreItems.length; const fullControlReady = coreReady && extendedGrantedCount === extendedItems.length; const summary = fullControlReady - ? "完整接管权限已具备" + ? "基础桌面控制和扩展能力权限已具备" : coreReady - ? "核心桌面控制已具备,完整接管待补齐" - : "核心桌面控制待授权,完整接管不可用"; + ? "基础桌面控制已可用,扩展权限按场景启用" + : "基础桌面控制待授权,桌面接管不可用"; return { coreReady, @@ -218,7 +219,7 @@ function resolvePermissionReadiness(coreItems, extendedItems) { extendedTotal: extendedItems.length, summary, detail: - "辅助功能、屏幕录制、自动化控制是桌面点击输入与画面识别的核心权限;完整接管还需要按业务场景补齐全磁盘访问、输入监控、通知、麦克风、摄像头和本地网络。", + "参考 Codex Computer Use 的最小权限模型,基础桌面控制只要求辅助功能和屏幕录制;自动化控制、全磁盘访问、输入监控、通知、麦克风、摄像头和本地网络都按具体任务场景再单独启用。", }; } @@ -229,31 +230,38 @@ function buildPermissionSetupPlan(coreItems, extendedItems, readiness) { description: item.description, tier: item.tier, status: item.status, - requiredForSilentControl: true, + requiredForSilentControl: item.tier === "core", canPreflight: AUTO_PREFLIGHT_PERMISSION_KEYS.has(item.key), settingsUrl: MACOS_PERMISSION_SETTINGS[item.key] ?? MACOS_PERMISSION_SETTINGS.all, openUrl: `/api/v1/boss-agent/permissions/open?target=${encodeURIComponent(item.key)}&returnTab=permissions`, owner: "boss-agent.app", })); - const missingActions = actions.filter((action) => action.status !== "granted"); + const missingRequiredActions = actions.filter( + (action) => action.requiredForSilentControl && action.status !== "granted", + ); + const optionalMissingActions = actions.filter( + (action) => !action.requiredForSilentControl && action.status !== "granted", + ); return { - mode: "one_time_setup", - title: "一次完整授权", - goal: "首次把完整接管需要的权限集中配置好,后续控制过程中只做状态校验和静默使用。", - silentUseReady: missingActions.length === 0, + mode: "minimal_computer_use", + title: "基础桌面控制授权", + goal: "按 Codex Computer Use 的思路,先拿辅助功能和屏幕录制两项最小权限;其他能力等任务需要时再申请。", + silentUseReady: missingRequiredActions.length === 0, primaryAction: { - label: "打开完整授权向导", - href: "/api/v1/boss-agent/permissions/open?target=all&returnTab=permissions", - settingsUrl: MACOS_PERMISSION_SETTINGS.all, + label: "打开基础授权", + href: "/api/v1/boss-agent/permissions/open?target=core&returnTab=permissions", + settingsUrl: MACOS_PERMISSION_SETTINGS.core, }, actions, - missingKeys: missingActions.map((action) => action.key), - summary: readiness.fullControlReady - ? "完整授权已满足,后续可静默执行。" - : "仍有权限未确认,请在首次配置阶段一次性补齐,避免后续任务执行中断。", + missingKeys: missingRequiredActions.map((action) => action.key), + missingRequiredKeys: missingRequiredActions.map((action) => action.key), + optionalMissingKeys: optionalMissingActions.map((action) => action.key), + summary: readiness.coreReady + ? "基础桌面控制已可用;扩展权限不会阻塞接管,只在对应任务需要时提示。" + : "仍缺少基础桌面控制权限,请先授权辅助功能和屏幕录制。", persistenceNote: - "macOS 会把授权持久写入系统隐私数据库;除非用户撤销授权、重装应用或更换运行时签名,否则后续控制不需要重复申请。", + "macOS 会把授权持久写入系统隐私数据库;稳定签名后,后续更新不会因为二进制哈希变化反复丢失授权。", }; } @@ -381,7 +389,11 @@ function setupActionRows(status) { return status.permissionSetup.actions .map((action) => { const tone = statusTone(action.status); - const preflight = action.canPreflight ? "可预触发" : "需手动开启"; + const preflight = action.requiredForSilentControl + ? "基础必需" + : action.canPreflight + ? "按场景预触发" + : "按场景启用"; return `
${escapeHtml(action.label)}
diff --git a/tests/boss-agent-status.test.mjs b/tests/boss-agent-status.test.mjs index d62234e..6f93eaa 100644 --- a/tests/boss-agent-status.test.mjs +++ b/tests/boss-agent-status.test.mjs @@ -55,8 +55,8 @@ test("boss-agent status exposes unbound QR binding and local permission states", assert.equal(status.skills.syncOk, true); assert.equal(status.permissionReadiness.coreReady, false); assert.equal(status.permissionReadiness.fullControlReady, false); - assert.match(status.permissionReadiness.summary, /完整接管/); - assert.equal(status.permissionSetup.mode, "one_time_setup"); + assert.match(status.permissionReadiness.summary, /基础桌面控制/); + assert.equal(status.permissionSetup.mode, "minimal_computer_use"); assert.equal(status.permissionSetup.silentUseReady, false); assert.equal(status.permissionSetup.actions.some((action) => action.key === "fullDiskAccess"), true); assert.equal( @@ -80,6 +80,11 @@ test("boss-agent status exposes unbound QR binding and local permission states", [ ["accessibility", "granted"], ["screenRecording", "missing"], + ], + ); + assert.deepEqual( + status.permissions.extendedItems.slice(0, 1).map((item) => [item.key, item.status]), + [ ["automation", "unknown"], ], ); @@ -179,10 +184,11 @@ test("boss-agent permission and skill menu entries render as separate tab pages" const permissionsHtml = renderBossAgentHtml(status, { activeTab: "permissions" }); assert.match(permissionsHtml, /class="active" href="\/boss-agent\?tab=permissions"/); - assert.match(permissionsHtml, /

一次完整授权<\/h2>/); - assert.match(permissionsHtml, /完整接管待补齐/); + assert.match(permissionsHtml, /

基础桌面控制授权<\/h2>/); + assert.match(permissionsHtml, /基础桌面控制已可用/); + assert.match(permissionsHtml, /扩展权限不会阻塞接管/); assert.match(permissionsHtml, /后续静默使用/); - assert.match(permissionsHtml, /api\/v1\/boss-agent\/permissions\/open\?target=all&returnTab=permissions/); + assert.match(permissionsHtml, /api\/v1\/boss-agent\/permissions\/open\?target=core&returnTab=permissions/); assert.match(permissionsHtml, /api\/v1\/boss-agent\/permissions\/open\?target=fullDiskAccess&returnTab=permissions/); assert.doesNotMatch(permissionsHtml, /

Skill 部署情况<\/h2>/); @@ -190,7 +196,45 @@ test("boss-agent permission and skill menu entries render as separate tab pages" assert.match(skillsHtml, /class="active" href="\/boss-agent\?tab=skills"/); assert.match(skillsHtml, /

Skill 部署情况<\/h2>/); assert.match(skillsHtml, /bb-browser/); - assert.doesNotMatch(skillsHtml, /

一次完整授权<\/h2>/); + assert.doesNotMatch(skillsHtml, /

基础桌面控制授权<\/h2>/); +}); + +test("boss-agent treats accessibility and screen recording as the minimal computer-use permission set", () => { + const status = buildBossAgentStatus( + { + deviceId: "macbook-air", + name: "MacBook Air", + account: "krisolo", + token: "boss-secret-token", + }, + { + lastHeartbeatOk: true, + }, + { + permissions: { + accessibility: "granted", + screenRecording: "granted", + automation: "missing", + fullDiskAccess: "missing", + inputMonitoring: "missing", + microphone: "missing", + camera: "missing", + localNetwork: "missing", + }, + }, + ); + + assert.equal(status.permissionReadiness.coreReady, true); + assert.equal(status.permissionSetup.silentUseReady, true); + assert.equal(status.permissionSetup.primaryAction.href, "/api/v1/boss-agent/permissions/open?target=core&returnTab=permissions"); + assert.match(status.permissionReadiness.summary, /基础桌面控制已可用/); + assert.equal(status.permissionSetup.actions.find((action) => action.key === "automation")?.requiredForSilentControl, false); + assert.equal(status.permissionSetup.actions.find((action) => action.key === "fullDiskAccess")?.requiredForSilentControl, false); + assert.deepEqual(status.permissionSetup.missingRequiredKeys, []); + assert.deepEqual( + status.permissions.items.map((item) => item.key), + ["accessibility", "screenRecording"], + ); }); test("boss-agent native permission overrides update app-owned camera and microphone state", () => { @@ -255,6 +299,7 @@ test("boss-agent mac app intercepts permission links and triggers native app per assert.match(swiftSource, /handleGetUrlEvent/); assert.match(swiftSource, /--request-permission/); assert.match(swiftSource, /--return-tab/); + assert.match(swiftSource, /target == "core"/); assert.match(swiftSource, /lastPermissionRequestTarget/); assert.match(swiftSource, /AXIsProcessTrustedWithOptions/); assert.match(swiftSource, /CGRequestScreenCaptureAccess/);