A from-scratch Swift 6 reimplementation of macOS computer-use tooling, built from a clean-room
reverse-engineering study of the Codex Computer Use.app service — with a drop-in
@oai/sky-compatible JS bridge and an MCP server.
中文简介:本项目对一个 macOS 上的 Computer Use 后台服务做了完整逆向(App 结构 / IPC 协议 / 权限与锁屏安全模型 / AX·截图·输入引擎),然后用 Swift 6 从零重写了一套功能等价的实现: 常驻服务 +
cua命令行 + MCP server + 与@oai/sky同形的 JS 客户端。 与 OpenAI 无任何关系,不包含其代码,不包含任何私有 API 调用。
| 组件 | 说明 |
|---|---|
CUAService |
LSUIElement 后台服务:AF_UNIX socket、JSON-RPC 2.0(4B LE 长度前缀)、按会话授权 App、权限门 |
CUAEngine |
无障碍树抓取(AXUIElement)+ 元素索引表、常驻 ScreenCaptureKit 窗口流(系统共享预览)、CGEventPostToPid 后台输入、应用与窗口枚举 |
PiP 预览 |
服务内 AppKit 浮窗(320×220 HUD)+ 菜单栏状态项,实时显示受控窗口、可拖动/停止共享/自动淡出 |
cua |
命令行:ping/apps/windows/state/click/press-key/type/scroll/set-value/drag/paste/select-text/action/screenshot/doctor/session/call |
cua mcp |
stdio MCP server,16 个工具(含 find_text / click_text) |
@swiftcua/sky |
Node 客户端:sky.list_apps()、sky.get_app_state() … 与原 @oai/sky 同形,另有 find_text/click_text |
swiftcua-mcp |
Node MCP server,提供 cua_repl 风格 js / js_reset / turn_ended 工具 |
git clone https://github.com/V1ki/swift-computer-use
cd swift-computer-use
make build # swift build
make test # 单元测试(不需要任何系统权限)
make js-test # Node 兼容层测试
make install # 组装并签名 SwiftCUA.app → ~/Applications + LaunchAgent首次安装后授权(一次性):
- 打开「系统设置 → 隐私与安全性 → 屏幕录制」,添加/勾选
SwiftCUA.app - 打开「系统设置 → 隐私与安全性 → 辅助功能」,添加/勾选
SwiftCUA.app launchctl kickstart -k gui/$(id -u)/dev.v1ki.swiftcua.service
助手:./scripts/open-permissions.sh 会直接打开这两个设置面板(授权仍需手点)。
验证:
~/Applications/SwiftCUA.app/Contents/MacOS/cua doctor
~/Applications/SwiftCUA.app/Contents/MacOS/cua ping
~/Applications/SwiftCUA.app/Contents/MacOS/cua apps
~/Applications/SwiftCUA.app/Contents/MacOS/cua state --app com.apple.Safari --screenshot-out /tmp/safari.jpg签名身份:
scripts/build-app.sh会自动挑选钥匙串里的第一个 Apple Development 身份; 也可用CUA_SIGN_IDENTITY="Apple Development: ..." make app指定。 用固定身份签名,TCC 授权才能在重编译后保留;ad-hoc 签名每次重建都会被系统当成新 App。
// 以前:import { sky } from "@oai/sky";
import { sky } from "@swiftcua/sky"; // 换成这一行即可
const apps = await sky.list_apps({ includeWindows: true });
const state = await sky.get_app_state({ app: "com.apple.Safari" });
console.log(state.text); // 带索引的无障碍树
await sky.click({ app: "com.apple.Safari", elementIndex: 12 });
await sky.type_text({ app: "com.apple.Safari", text: "hello" });
await sky.press_key({ app: "com.apple.Safari", key: "Control_L+l" });安装:
cd js && npm link # 或 npm install /path/to/swift-computer-use/js把服务暴露给任意 MCP 客户端(Codex / Claude Desktop / 其它):
{
"mcpServers": {
"swiftcua": {
"command": "$HOME/Applications/SwiftCUA.app/Contents/MacOS/cua",
"args": ["mcp"]
}
}
}或使用 Node 版(额外提供 js 沙箱工具,和原 cua_repl 同形):
{
"mcpServers": {
"swiftcua-repl": {
"command": "node",
"args": ["/path/to/swift-computer-use/js/bin/mcp.mjs"]
}
}
}工具清单与参数见 js/README.md 与 cua mcp 的 tools/list。
完整研究见
re/05-ax-less-apps.md。
- 先激活再读 AX:很多国产 App(微信、部分 Electron/CEF 壳)是「懒 AX」——不在前台时
只暴露菜单栏。SwiftCUA 的
getAppState/findText检测到根不是AXWindow会自动激活重抓。 - AX 可用就用语义操作:微信激活后有会话列表(
list "会话",每行一个staticText)、 消息列表、输入框、发送按钮,可click --element N/click-text --text 通讯录。 - AX 不可用再退像素:
capture_screenshot+find_text(Vision OCR)拿窗口相对坐标,click --x --y/click_text点击;键盘快捷键(press_key)优先于坐标。 - 发消息这类不可逆动作要人工确认,
发送按钮在输入框为空时是disabled,可作安全断言。
cua state --app com.tencent.xinWeChat # 后台读取(不抢前台)
cua --activate click-text --app com.tencent.xinWeChat --text 通讯录 # 显式允许 raise 后再点
cua find-text --app com.tencent.xinWeChat --text 通讯录
cua click-text --app com.tencent.xinWeChat --text 通讯录 # 切到通讯录页
cua find-text --app com.tencent.xinWeChat --text 搜索 --ocr-only # 强制 OCR 定位默认不抢前台、不移动物理光标:
- 语义动作走 AX(
AXPress/AXValue/ selection),对后台 App 生效; - 坐标与键盘走
CGEventPostToPid定向投递(目标 App 收事件,前台 App 与真实光标不受影响); - 需要「真的把窗口拿到前台」时才用
cua --activate <command>(或在 MCP/JS 里set_options {activate:true})。
截图走常驻 SCStream(每窗口 2fps,空闲 20s 自动停),因此 macOS 会显示系统「正在共享」预览。
另外服务自带一个��绘 PiP 小窗(对应原版的 RemoteHostedPIP):320×220 HUD 面板 + 菜单栏状态项
(viewfinder 图标),实时显示受控窗口画面、标题、停止共享 / × 按钮;可拖动并记住位置,
无帧 3 秒后淡出。面板 canBecomeKey/Main = false + nonactivatingPanel,不会抢焦点或光标。
cua-service --headless(或 SWIFTCUA_HEADLESS=1)可完全关闭 UI。
cua state --app com.tencent.xinWeChat # 后台读取,Chrome 保持前台
cua click-text --app com.tencent.xinWeChat --text 通讯录 # 后台投递(部分 App 不响应鼠标)
cua --activate click-text --app com.tencent.xinWeChat --text 通讯录 # 显式允许接管前台已知边界:微信侧边栏等按钮不响应 pid 定向的鼠标事件(原版靠私有合成焦点让它们响应), 这类操作在后台会「成功但界面不变」,需要
--activate;纯 AX 操作(写输入框、部分按钮)后台可用。
| 报告 | 内容 |
|---|---|
re/01-bundle-and-binary.md |
App bundle 结构、6 个 Mach-O、签名/公证、符号面、Sparkle/Statsig、BUILD.bazel 泄露的模块划分 |
re/02-protocol.md |
客户端↔服务协议:4B LE + JSON-RPC 2.0、ping 握手、14 帧实测、11 个方法参数形状、MCP 工具 schema |
re/03-security-model.md |
TCC 权限矩阵、连接级签名/审计校验、会话与 Always-allow 落盘、锁屏授权插件与 Ctrl-Cmd-Q 回锁 |
re/04-engine-internals.md |
截图(SkyLight/SCScreenshotManager)、AX 树(公开 API + 私有 AccessibilitySPI)、输入注入(CGEventPostToPid)、AX 文本格式 |
re/05-ax-less-apps.md |
懒 AX / 无 AX 应用的 AX 可用性矩阵、激活重试、OCR 兜底、快捷键与坐标控制策略 |
re/06-background-and-preview.md |
后台不抢前台:CGEventPostToPid 能力边界、SCStream 系统共享预览、原版合成焦点/自绘 PIP 证据 |
re/raw/里的原始 dump 有数百 MB,未入库;已入库的只有re/raw/*/run.sh等小体积复现脚本(见 re/README.md)。
- 测试:40 个 Swift 单元测试 + 21 个 JS 测试 +
scripts/smoke.sh端到端冒烟,全部通过。 - 协议:安装版服务 ping / list_apps(327) / 会话授权 TOML / 权限门错误码实测通过。
- 引擎:开发版服务(继承终端 TCC)实测 AX 树 1233 元素、
--query过滤、窗口映射与 2560×1410 JPEG 截图。 - 待用户授权:安装版服务的屏幕录制/辅助功能授权、真实鼠标键盘注入(会抢占前台,未自动执行)。
完整证据见 docs/VALIDATION.md。
docs/ARCHITECTURE.md—— 进程图、模块边界、四条引擎管线、与原实现的差异docs/PROTOCOL.md—— 完整线协议:分帧、握手、方法表、错误码、超时docs/COMPATIBILITY.md——@oai/sky方法映射与已知差异docs/VALIDATION.md—— 本机实测记录(测试、AX 树、截图、安装状态)
后台模式有运行时守卫:任何全局 CGEvent 注入都会直接报错(--activate 才允许),
launchApp 不会抢焦点,AX 写值/键盘走 CGEventPostToPid。
# 诊断:把每一步的光标坐标/前台 PID/事件计数写进 unified log
SWIFTCUA_TRACE_CURSOR=1 cua state --app com.apple.TextEdit
# 自带验证脚本(默认一次性 TextEdit 文档;--wechat-self-chat 仅用于 knight 自聊)
./scripts/verify-background.sh- socket 权限
0600,只允许同用户进程;可选的对端签名校验(--require-peer+config.json白名单)。 - 每次会话按 App 授权,落盘 TOML;
autoAuthorizeApps可关闭以要求显式授权。 - 不联网、无遥测、自动更新关闭;截图与 AX 文本只走本机 socket。
- 刻意不实现锁屏操作与提权自动化(原服务通过
/Library/Security/SecurityAgentPlugins做到这一点,风险过高)。
| 项 | 原服务 | SwiftCUA |
|---|---|---|
| 私有 API | SkyLight / WindowServerSPI / AccessibilitySPI | 仅公开 API |
| 锁屏操作 | 支持(授权插件) | 不支持 |
| 协议字节兼容 | — | 否(仅分帧一致) |
| App 发现 | Spotlight MDQuery | NSWorkspace |
| 更新/遥测 | Sparkle + Statsig | 无 |
Sources/CUACore/ 协议、模型、帧、错误码、路径
Sources/CUAEngine/ AX 树、截图、输入、应用/窗口目录
Sources/CUAService/ socket 服务、鉴权、会话、路由
Sources/CUAClient/ cua CLI + MCP server
js/ @swiftcua/sky + swiftcua-mcp(零依赖)
re/ 逆向报告(raw dump 不入库)
docs/ 架构、协议、兼容性
scripts/ 构建、签名、安装、卸载、开发服务
swift build && swift test # 主包
cd js && node --test test/ # JS 桥
./scripts/dev-service.sh # 前台跑服务(隔离 ~/.devhome)| 现象 | 处理 |
|---|---|
cua doctor 报 socket 不存在 |
launchctl kickstart -k gui/$(id -u)/dev.v1ki.swiftcua.service,看 ~/Library/Logs/SwiftCUA/service.err.log |
permissionMissing: ScreenRecording |
系统设置里勾选 SwiftCUA.app,然后重启服务 |
elementIndexStale |
重新 get_app_state,索引表随每次捕获重建 |
| 右键菜单/弹窗拿不到 | 弹窗是独立窗口,先 list_windows 找到它再定向操作 |
MIT(见 LICENSE)。 本项目仅用于学习与研究;逆向部分只做只读分析,未修改、未分发原 App 的任何代码或资源。 "Codex"、"OpenAI" 是其各自权利人的商标,本项目与其无隶属或背书关系。