资讯详情

在 macOS 应用中零额外权限嵌入 cua-driver:基于 TCC 责任链的嵌入式宿主集成指南

📅 2026/9/14 23:00:39 | 华诺云谱 👁 阅读
在 macOS 应用中零额外权限嵌入 cua-driver:基于 TCC 责任链的嵌入式宿主集成指南
在 macOS 应用中零额外权限嵌入 cua-driver基于 TCC 责任链的嵌入式宿主集成指南【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code本指南面向希望在自己的 macOS Agent 应用agent harness内部集成 cua-driver 后台计算机使用能力与 agent 光标叠加层的开发团队。核心目标是不额外分发一个应用包、用户永远看不到第二次 macOS 权限弹窗——宿主应用只需请求一次「辅助功能」与「屏幕录制」授权内嵌的驱动便自动继承这些授权。读完本文你将掌握 TCC 责任链机制、直接运行时与 daemon 宿主两种嵌入路径、三种代理权限模式以及一个可复制运行的完整 Swift 参考宿主。本文以 cua-driver 技能包中的 EMBEDDING.md 为骨架并结合 cua-driver 相关源码进行印证。macOS 如何归属这些权限嵌入前必须了解macOS 的 TCCSystem Settings → Privacy Security 背后的隐私系统并不把辅助功能Accessibility或屏幕录制Screen Recording归属到可执行文件路径上而是归属到责任进程responsible process——即由内核/LaunchServices 追踪的进程启动链顶端的那个 app。当你的已签名 app 通过posix_spawn、NSTask/Process或原生fork/exec派生子进程时该子进程始终留在你的责任链内——子进程发起的 TCC 检查会用你 app 的授权来应答任何由此触发的权限弹窗也会显示你 app 的名字。这正是嵌入所依赖的行为宿主授权一次所有行为良好的子进程自动继承。你可以用以下命令实时观察归因链log stream --debug --predicate subsystem com.apple.TCC AND eventMessage BEGINSWITH AttributionChain有两件事会打断这条链而内嵌驱动必须且在嵌入模式下确实不做通过 LaunchServices 启动open -a …、NSWorkspace.open会让被启动的应用成为自己的责任进程进程可以显式放弃对子进程的责任responsibility_spawnattrs_setdisclaim使子进程成为自己的责任进程——独立版 cua-driver 正是刻意这么做的让权限归属于稳定的com.qwencode.cua-driver身份而不是启动它的终端。嵌入模式会关闭这一行为。需要特别说明的是这是 TCC责任responsibility继承与 App Sandbox 的继承com.apple.security.inherit无关。本指南假设宿主是未启用沙箱的应用agent harness 的典型形态沙箱宿主派生非沙箱 helper 会引出另外的 App Sandbox 问题不属于嵌入模式解决的范围。从源码看嵌入模式的开关定义在 cua-driver-core/src/lib.rs 中pub const EMBEDDED_ENV: str CUA_DRIVER_EMBEDDED; pub const PARENT_LIVENESS_STDIN_ENV: str CUA_DRIVER_PARENT_LIVENESS_STDIN;只有当CUA_DRIVER_EMBEDDED恰为1时才启用嵌入模式且父进程存活管道parent-liveness pipe只在直接嵌入的 daemon场景下有效——这为后面「宿主死亡即关闭 daemon」的生命周期规则提供了源码级依据。首选方案同进程运行时直接使用 SDKPython 与 TypeScript 应用通常应直接导入打包好的 SDK 并创建CuaDriver。这条路径不会启动可执行程序、不打开 socketTCC 检查以导入方应用的身份执行import { CuaDriver } from qwen-code/cua-sdk; const driver CuaDriver.create(undefined); try { const metadata await driver.metadata(); // 在这里调用类型化的驱动操作 } finally { await driver.shutdown(); driver.uniffiDestroy(); }直接运行时永远不会呈现 macOS 权限 UI。即使是check_permissions({prompt: true})也会被强制降级为只读检查并把宿主报告为责任权限所有者。注意宿主变更了辅助功能或屏幕录制授权后必须完全重启宿主再创建替代运行时。两个使用边界AppKit agent 光标叠加层在任意直接运行时中不可用。在宿主安装经过认证的主线程 UI 适配器之前叠加层方法返回结构化的facility_unavailable结果。需要可见叠加层时请使用私有 worker 或 daemon 支撑的宿主。daemon 支撑宿主仅在以下情况使用应用还必须向外部 Agent 提供稳定的 MCP 端点、需要协调外部客户端或需要将自动化运行时与应用进程隔离。启动 daemon 支撑的宿主# 环境变量形式 —— 由宿主设置在子进程上 CUA_DRIVER_EMBEDDED1 CUA_DRIVER_HOST_BUNDLE_IDcom.yourco.yourapp \ qwen-cua-driver serve --socket /tmp/yourapp-cua.sock # daemon socket 就绪后启动 stdio MCP 代理 CUA_DRIVER_EMBEDDED1 qwen-cua-driver mcp --socket /tmp/yourapp-cua.sock宿主侧必须满足的要求直接把qwen-cua-driver serve --embedded作为子进程派生用Process/NSTask、posix_spawn或从你自己的代码里exec。不要通过open(1)或NSWorkspace启动 daemon——那会把它交给 LaunchServices破坏继承。给 daemon 一个私有 socket并等待它开始接受连接。派生qwen-cua-driver mcp --embedded --socket path并通过该代理的 stdin/stdout 以行分隔 JSON-RPC与 MCP 通信。代理从不执行工具真正执行工具的是宿主拥有的 daemon。在启动驱动之前或之后——在此之前驱动只报告未授权从你的 app请求辅助功能与屏幕录制分别使用AXIsProcessTrustedWithOptions([kAXTrustedCheckOptionPrompt: true])和CGRequestScreenCaptureAccess()。环境变量的精确语义fail-safe 设计只有精确值CUA_DRIVER_EMBEDDED1才启用嵌入模式其他任何值都会被忽略fail-safe。--host-bundle-id只是一个咨询性标签会回显在check_permissions输出和日志中——它不是信任信号信任来自 OS 责任链因此设置它没有任何可伪造的空间。CLI 参数在 cua-driver/src/cli.rs 中有完整定义--embedded声明嵌入宿主模式无--direct时要求通过--socket连接宿主的私有服务而非自动启动独立 app、--host-bundle-id为咨询标签、--dangerously-bypass-approvals选择不受限模式并记录确认。--embedded/--host-bundle-id会被导出到环境变量传递给子进程与CUA_DRIVER_EMBEDDED机制一致。选择代理权限模式嵌入式宿主拥有自己面向用户的权限体验但必须在可信启动时选择 cua-driver不可变的运行时模式模式含义standard无提示的常规自动化仅对残余边界如已登录的 Chromium 配置进行显式宿主授权bounded在已批准的工具与资源清单内进行无人值守工作unrestricted无任何 Cua 运行时审批仅当宿主接受提示注入与非预期操作的后果时使用直接 SDK 宿主可安装DriverAuthorizationHost处理标准模式下的残余决策、DriverActivityObserver处理无内容的动作事件。cua-driver不渲染自己的授权弹窗或横幅。对于不受限嵌入使用显式的两部分环境契约CUA_DRIVER_EMBEDDED1 \ CUA_DRIVER_PERMISSION_MODEunrestricted \ CUA_DRIVER_DANGEROUSLY_BYPASS_APPROVALS1 \ qwen-cua-driver serve --embedded --socket /tmp/yourapp-cua.sock两个值缺一不可且矛盾的值会在 daemon 绑定之前就失败。它们应放在可信的启动器配置中绝不暴露为 Agent 可设置的 MCP 或裸 socket 参数。交互式操作员可用等效的单条 CLI 快捷方式--dangerously-bypass-approvals它会选择不受限模式并记录确认。迁移期间旧的autonomous模式名仍作为bounded的别名被接受。Node 与 Electron daemon 宿主用qwen-code/cua-sdk中的嵌入式宿主而不是在每个宿主里各自实现进程与 socket 管理。它直接启动私有 daemon、等待 socket 接受连接、返回 SDK 与 MCP 连接信息并负责重启与清理import { CuaDriver, EmbeddedCuaDriverHost } from qwen-code/cua-sdk; const embedded new EmbeddedCuaDriverHost( /path/inside/YourApp.app/Contents/Resources/qwen-cua-driver, com.example.your-app ); const connection await embedded.start(); const driver CuaDriver.connect(connection.socketPath); // 应用调用使用 driverAgent 运行时使用 connection.mcp driver.uniffiDestroy(); await embedded.stop(); embedded.uniffiDestroy();EmbeddedCuaDriverHost的start/stop/restart/wait_for_exit等 API 定义于 cua-driver-sdk/src/embedded.rsNode 侧为waitForExit(generation)Python 侧为wait_for_exit(generation)。关键打包与签名要点该包不负责安装或捆绑cua-driver。你需要自行分发一个兼容的可执行文件放在 Electron 的 ASAR 归档之外、保留其可执行位并在签名和公证外层 macOS 应用之前对嵌套可执行文件签名。Electron 主进程可在app.whenReady()之后使用包的/electron入口做底层辅助功能与屏幕录制请求这些调用以导入方宿主身份运行而非子驱动。宿主仍拥有权限 UI、状态与重启策略。这些函数使用同一套生成的 Rust SDK没有第二个原生 FFI 依赖。部分 macOS 版本拒绝弹出屏幕录制提示此时用openMacOSScreenRecordingSettings()打开屏幕录制设置面板请用户添加宿主 app并在两项检查都返回 true 后再启动驱动。生命周期规则并发的start()调用会合并为同一代generationdaemon。把connection视为按代generation作用域的对象。restart()之后销毁旧的 SDK 客户端与 MCP 代理用新返回的描述符重新连接。停止时按序执行停止新工作 → 结束会话 → 关闭代理/客户端 →await stop()。stop()是幂等的且会取消进行中的 start。用waitForExit(generation)Node/wait_for_exit(generation)Python观察异常终止。绝不盲目重放一个完成状态未知的动作。Rust 所有者持有父进程存活管道CUA_DRIVER_PARENT_LIVENESS_STDIN1宿主死亡即关闭 daemon但有序关闭仍应await stop()。捕获模态capture modality属于每个观察/动作目标不属于生命周期会话。一个嵌入 daemon 可以并发服务精确窗口调用与桌面调用无需改变会话状态。权限变化要求销毁客户端 → 重启 daemon → 重新连接。旧一代的连接永不可复用。嵌入模式改变了什么以及不改变什么维度独立Standalone嵌入CUA_DRIVER_EMBEDDED1责任放弃重执行disclaim re-exec开拥有自己的 TCC 身份关留在宿主的责任链中工具执行进程servedaemon宿主派生的serve --embeddeddaemon通过open -a QwenCuaDriver自动重启 daemon已安装时是永不会脱离宿主的责任链TCC 身份com.qwencode.cua-driver宿主 app权限弹窗 / 启动门槛可能弹一次永不弹窗Settings → Privacy Security 条目QwenCuaDriver仅你的 appcheck_permissions的source.attributiondriver-daemon或callerhost叠加层、后台输入、采集、全部工具完整完整——完全一致其他一切——agent 光标叠加层、后台不抢焦点的点击与键入、AX 树读取、逐窗口截图——保持不变。当嵌入模式关闭时本特性没有任何部分处于激活状态独立行为与之前逐字节一致。责任链要求的精确表述宿主必须是驱动的责任进程。当你直接派生servedaemon 且嵌入模式开启时这一点自动成立。如果允许 daemon 放弃责任独立行为macOS 会把它当作自己的责任进程用户会看到第二个归属到驱动二进制的弹窗、第二个 Settings 条目且采集/AX 在第二次授权之前一直失败——这正是嵌入要消除的体验。嵌入模式短路了放弃责任重执行对应源码中的responsibility.rs与open -a CuaDriverdaemon 重启。MCP 永远是代理因此嵌入 daemon 仍是唯一检查 TCC 并执行工具的进程。App 网关架构--embedded不会把 GUI 应用的权限转移给驱动它只是让驱动留在其派生者的 TCC 责任链内。如果你的产品有一个拥有 macOS 授权的 GUI app另有独立的网关/daemon/Node 进程来派生 MCP server那么把qwen-cua-driver serve --embedded注册给网关会让 daemon 继承网关的身份而不是 app 的。正确做法是从 GUI app 自身派生 daemon网关可以连接 MCP 代理到 app 拥有的私有 socket错误继承网关身份 正确 gateway / node daemon YourApp.app └─ qwen-cua-driver serve --embedded ├─ qwen-cua-driver serve --embedded └─ qwen-cua-driver mcp --embedded --socket private-path注意check_permissions无法检测这种错误——只要设置了CUA_DRIVER_EMBEDDED1source.attribution就报告host即使驱动是网关派生的。症状表现为授权布尔值跟随网关的 TCC 状态、弹窗/Settings 条目显示网关进程名详见文末 Troubleshooting。从宿主读取check_permissions通过 MCP 调用check_permissions工具。在嵌入模式下它绝不弹对话框prompt参数被忽略返回{ accessibility: true, screen_recording: true, screen_recording_capturable: null, direct_capture_status: not_checked, source: { attribution: host, host_bundle_id: com.yourco.yourapp, embedded: true, pid: 12345, responsible_ppid: 12300, executable: /path/to/qwen-cua-driver, disclaim_env: false, note: Embedded mode: these booleans reflect the HOST apps TCC grant… } }字段解读accessibility/screen_recording—— 从驱动进程内部与宿主共享身份应答的你 app 授权的实时 TCC 状态。两者都为 true 即可安全驱动桌面。screen_recording_capturable/direct_capture_status—— 嵌入的check_permissions是只读的从不运行 Tahoe 的可弹窗 ScreenCaptureKit 探测因此为null/not_checked。宿主拥有权限 UX应在向用户解释授权后用一次显式截图或采集操作来验证像素可达。source.attribution的取值host—— 嵌入模式布尔值反映宿主的授权。嵌入时你应始终看到它。driver-daemon—— 独立 daemon 拥有com.qwencode.cua-driver。嵌入时若看到它说明嵌入模式实际上未生效。caller—— 非嵌入、非 bundle 启动例如有人在终端里直接运行二进制布尔值反映终端的授权。如果某项权限缺失正确的反应是宿主去请求它上面的两个 API 调用然后重新调用check_permissions。驱动从不拥有宿主的 OS 权限体验。授权时机提醒macOS 会按进程缓存 TCC 应答。如果你的 app 是在驱动子进程已经运行之后才请求/获得授权的请重启驱动子进程让它以全新缓存重新查询。最小宿主示例可直接复制仓库中的 ExampleAgentHarness.swift 是完整的参考宿主与本文档镜像同步另有覆盖 TCC 重置流程的构建运行脚本 demo.sh。它以宿主身份请求两项授权、派生嵌入 daemon 加 MCP 代理并完整跑通演示序列归因检查、后台截图、后台 AX 读取、agent 光标滑动。核心流程如下完整源码见上述文件以宿主身份请求两项授权整个流程中唯一的弹窗使用AXIsProcessTrustedWithOptions与CGRequestScreenCaptureAccess()无授权也继续运行以便一轮跑出两项 Settings 条目授权后重跑。直接派生 daemon 子进程绝不通过open/NSWorkspace设置CUA_DRIVER_EMBEDDED1与CUA_DRIVER_HOST_BUNDLE_ID参数为serve --embedded --socket path轮询等待 socket 文件出现带 10 秒截止再派生mcp --embedded --socket path代理。通过子进程 stdio 走行分隔 JSON-RPC 2.0先发initialize协议版本2024-11-05notifications/initialized再以tools/call调用工具。check_permissions必须报告attribution: host且不弹窗。后台 AX 读 窗口截图launch_app不置前台解析 pid 与窗口→get_window_state返回 AX 元素树和后台窗口截图。agent 光标滑动move_cursor两次观察叠加层滑动而真实指针不动。构建为已签名的 app bundle稳定的签名身份是 TCC 授权行键到你的 app 的依据mkdir -p ExampleAgentHarness.app/Contents/MacOS swiftc -O ExampleAgentHarness.swift \ -o ExampleAgentHarness.app/Contents/MacOS/ExampleAgentHarness \ -framework ApplicationServices printf %s\n ?xml version1.0 encodingUTF-8? \ !DOCTYPE plist PUBLIC -//Apple//DTD PLIST 1.0//EN http://www.apple.com/DTDs/PropertyList-1.0.dtd \ plist version1.0dict \ keyCFBundleExecutable/keystringExampleAgentHarness/string \ keyCFBundleIdentifier/keystringcom.qwencode.example-agent-harness/string \ keyCFBundlePackageType/keystringAPPL/string \ /dict/plist ExampleAgentHarness.app/Contents/Info.plist codesign --force --sign - ExampleAgentHarness.app # 生产环境请使用你的 Developer ID open ExampleAgentHarness.app # 这里的 open 是正确的宿主自身必须是责任进程 tail -f /tmp/cua-embedded-demo.logdemo.sh 还封装了完整的 TCC 重置—授权—重跑流程重建即重签名新签名可能孤立旧授权行所以同一构建贯穿授权重跑流程、tccutil reset重置、通过launchctl setenv把自定义驱动路径传给open启动的 app、轮询日志等待DEMO COMPLETE: PASS。注意该脚本是手动的——TCC 弹窗无法在 CI 中授权必须有一台真实 Mac 和键盘前的用户。Troubleshooting「我仍然看到第二个权限弹窗 / 第二个 Settings 条目」做 TCC 检查的进程上嵌入模式未生效。按可能性排序的原因(a)CUA_DRIVER_EMBEDDED不是精确的1或设置在了你的 app 上但没传进 daemon 子进程的环境(b) daemon 是通过open(1)/NSWorkspace而非直接派生启动的因此它是自己的责任进程(c) MCP 代理连接到了旧的独立QwenCuaDriver.appdaemon——检查check_permissions→source.attribution必须是host并确认代理用的是宿主的私有 socket。要精确看到 macOS 把身份算给了谁运行log stream --debug --predicate subsystem com.apple.TCC AND eventMessage BEGINSWITH AttributionChain并再次触发该动作。「screen_recording: true但截图全黑」只读的权限检查无法在不冒险弹系统对话框的前提下验证直接的 ScreenCaptureKit 访问。仅在宿主解释了 OS 授权之后执行一次显式截图。若仍失败授权可能不属于驱动当前的责任身份、可能被重置或驱动逃出了宿主的责任链见上一条。授权变化后重启驱动子进程——TCC 应答按进程缓存。「AX 树为空 / 点击无反应」AXIsProcessTrusted()对当前生效身份返回 false。可能原因宿主未获辅助功能授权或授权发生在驱动子进程启动之后又是按进程缓存——重启子进程或 app 被重新签名/移动导致既有授权行不再匹配在 System Settings 中删除重加或tccutil reset Accessibility your-bundle-id后重新授权。「本来能用更新/重签名 app 后失效了」TCC 授权行以 app 的代码签名身份为键。签名变化会孤立旧授权行。重置并重新授权tccutil reset Accessibility com.yourco.yourapp tccutil reset ScreenCapture com.yourco.yourapp平台说明Windows / Linux嵌入在Windows 与 LinuxX11上同样可用且没有逐 app 的权限仪式。宿主仍然在目标交互会话或桌面上派生 daemon然后把 MCP 与 CLI 适配器指向其私有 socket。本指南中的一次授权继承故事是 macOS 特有的因为只有 macOS 的辅助功能与屏幕录制授权才跟随责任链。两个已知例外Windows提权 / UWP 目标向更高完整性integrity级别的目标注入像素或 SendInput需要一个交互启动的 High-IL daemon安装的自启任务使用RunLevelHighest。cua-driver-uia管道是保留的、默认关闭的 daemon 内部边界嵌入宿主及其他公共客户端不得启动或连接它。Linux Wayland随合成器而异采集走 XDG desktop portals在采集时按会话弹窗宿主无法预先授权。X11 没有 portal 门槛。小结嵌入模式的价值可以浓缩为一句话授权一次给宿主行为良好的子进程全部继承。它通过关闭独立模式下的责任放弃重执行与open -a自动重启让 daemon 永远留在宿主的 TCC 责任链中MCP 永远是代理唯一执行工具、检查 TCC 的进程就是宿主派生的serve --embeddeddaemon。无论是选择同进程的 SDK 直接运行时还是需要 MCP 端点/进程隔离时采用 daemon 支撑宿主本文的配置示例、权限模式、生命周期规则与最小 Swift 宿主见 examples/embedded-host-macos都可以直接作为你集成的起点。进一步可阅读技能包内其他平台文档MACOS.md、WINDOWS.md、LINUX.md以及 cua-driver 主 README。【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。