Claude Code CLI 2.1.235 Native Bridge 与 JavaScript Runtime¶
Claude Code 的主控制面在 bundled JavaScript 中,但图像、音频、键鼠、截图、应用/TCC 和 URL event 等能力通过 5 个 .node 模块进入 macOS 原生框架。要理解这些功能,必须把三层连起来:JavaScript 调用点定义产品语义,N-API export 定义 ABI 合同,Rust/Swift/Mach-O 分析解释底层实现。
60 秒理解原生桥¶
读者问题: 为什么导出函数名对上了还不算重建完成,截图、录音或键鼠调用失败时又应该查 JavaScript、N-API、线程、TCC 还是系统框架?
一句话模型: JavaScript wrapper 规定产品可见的参数、返回值和生命周期,N-API 规定二进制合同,Rust/Swift 对象调用 macOS 框架并承受线程、内存和 TCC 约束;兼容重建必须同时匹配这三层,而不是只复制 export 表。
贯穿场景:Computer Use 请求截取窗口并发送鼠标点击。JS 先用 wrapper 构造参数;N-API 创建原生对象;Swift 截图路径访问 ScreenCaptureKit/CoreGraphics 并检查授权;Rust 输入模块产生真实鼠标副作用;结果回到 JS 后被转成模型可见字段。截图失败可以重试或提示授权,已经发出的点击却不能靠 JS exception 回滚。
| 层 | 合同对象 | 需要匹配的细节 | 失败表象 | 恢复/副作用边界 |
|---|---|---|---|---|
| JavaScript consumer | 函数调用、参数和产品状态 | nullability、Promise/callback、字段语义、调用顺序 | 参数看似正确但 UI/Agent 行为不对 | wrapper 决定何时重试和如何展示 |
| N-API ABI | exports、object/prototype、native handle | 类型、异常、线程安全、finalizer、架构 | load error、crash、对象失效 | export count 相同仍可能不兼容 |
| Rust/Swift runtime | buffer、线程、资源对象 | bytes、采样率、图像格式、stop/dispose | 泄漏、竞态、错误字段不一致 | 必须显式释放长期资源 |
| macOS/TCC | framework 与授权状态 | permission、display/window/app identity、event delivery | 无画面、无音频、输入被拒绝 | 授权和系统状态在模块外部 |
| 外部副作用 | 键鼠、录音、URL event | 执行顺序和幂等性 | 调用失败前动作已部分发生 | 普通异常不能撤销真实输入 |
后文按五个模块逐一连接 JS consumer、N-API contract、Mach-O/语言证据、资源生命周期和双跑 Probe,并始终保留 Observed、Derived、Compatible 三类结论。
五个 native module 在运行时的位置¶
reverse/javascript/cli.readable.js 45-57 通过 Bun standalone 虚拟路径加载:
| Module | 语言证据 | JavaScript 侧用途 |
|---|---|---|
image-processor.node |
Rust + image ecosystem |
图像 metadata、resize、JPEG/PNG/WebP、clipboard image |
computer-use-swift.node |
Swift + AppKit/ScreenCaptureKit/CoreGraphics | 截图、显示器、应用发现、TCC、ESC hotkey |
computer-use-input.node |
Rust + Enigo/AppKit | 键盘、鼠标、滚轮、前台应用 |
audio-capture.node |
Rust + CPAL/CoreAudio/AVFoundation | 录音、播放、PCM、麦克风授权 |
url-handler.node |
Rust + Carbon Apple Events | 等待 GURL URL event |
bundled JS
|
| require('/$bunfs/root/<module>.node')
v
N-API exports / objects / prototypes / Promise callbacks
|
v
Rust or Swift runtime
|
v
macOS frameworks / devices / TCC / event loop
Native module 不是独立服务。它们在同一个进程地址空间内运行;panic、ABI mismatch、线程/回调错误可以直接影响 CLI 进程。
三种证据等级不能混淆¶
- Observed:发布字节、N-API export、Mach-O import/export、字符串、Swift demangle、反汇编或原版 probe 直接证实。
- Derived:由调用参数、上下游结构和底层依赖推导。
- Compatible:独立重写后通过行为双跑,但不等于上游原函数体。
完整逐模块分类见 reconstructed/EVIDENCE.md。没有源码级 DWARF/source map 时,原始注释、局部名、文件边界、泛型写法和优化删除的分支无法逐字恢复。
JavaScript wrapper 为什么是产品合同¶
.node export 只说明函数存在。真正的产品语义还由 JS wrapper 决定:
- 何时 lazy load;
- 不支持平台时返回 null、false 还是抛错;
- 参数如何归一化;
- Promise/stream callback 如何接入 React/Agent Loop;
- 资源何时 dispose;
- native error 如何转成用户提示;
- 权限检查发生在调用前还是 native 内部。
因此 native 逆向不能只输出 symbol/strings。每个 export 都要回到主 JS 找 consumer。
Image processor:一次性资源对象¶
JavaScript wrapper 在 reverse/javascript/cli.readable.js 198380-198392:
- 读取 native module;
- module 不可用时抛
Native image processor module not available; - 调用
processImage(input)得到 nativeImageProcessor; - 再调用
metadata/resize/jpeg/png/webp/toBuffer/dispose。
原版 probe 证实 transform 方法返回同一 JavaScript 对象,支持链式调用;toBuffer 或 dispose 后再次访问会返回统一 consumed error。这说明对象持有 native buffer/decoder state,toBuffer 是消费边界,而不是无状态纯函数。
生命周期¶
input Buffer/Data URL
-> processImage
-> native ImageProcessor owns decoded state
-> resize/encode mutate or configure same object
-> toBuffer OR dispose
-> consumed; later calls fail
用户影响:忘记消费/释放会延长内存占用;重复调用不能假设幂等。重建验证对固定 1x1 RGBA PNG 的 JPEG、PNG、WebP 输出做了精确字节比较。
Computer Use Swift:高层 macOS 能力树¶
computer-use-swift.node 顶层导出 computerUse,再分为:
screenshot.captureExcluding/captureRegion;apps.listInstalled/listRunning/open/iconDataUrl/appUnderPoint/...;tcc.checkAccessibility/checkScreenRecording/request...;display.listAll/getSize;hotkey.registerEscape/unregister/notifyExpectedEscape;resolvePrepareCapture与测试/run-loop helper。
每个架构保留 1,237 个 demangled Swift symbols。静态证据显示 ScreenCaptureKit、AppKit、CoreGraphics、Spotlight metadata、CGEvent tap 与 TCC API。
截图不是简单 CGDisplayCreateImage¶
调用链需要处理 display、window/application exclusion、目标尺寸、JPEG quality、权限和异步 capture。结果字段包含 base64、width、height;base64 是裸 JPEG,不带 Data URL prefix。连续桌面帧不稳定,因此 probe 验证字段、尺寸、JPEG magic,并用对应原版/重建版 image-processor.node 实际解码格式与宽高,不比较两次真实截图的完整 SHA-256。
App discovery 的微小语义¶
- installed apps 使用 Spotlight metadata;
- 保留 Spotlight 顺序和重复 bundle ID;
- Spotlight query 无法启动时 rejected Promise 携带稳定错误,不以文件系统扫描结果静默代替;
- icon 路径重绘为透明 64x64 PNG;
appUnderPoint可返回 Dock 等覆盖窗口;- 结果是
{bundleId, displayName},不额外返回pid。
这些小行为决定 UI/自动化调用者的兼容性,不能被“列出应用”四个字替代。
TCC 是显式状态,不是异常文本¶
Accessibility 与 Screen Recording 有 check/request 两套调用。用户拒绝、未决定、已允许应映射成稳定状态;请求授权通常需要主线程/run loop 协调。Native bridge 不能把 TCC dialog 当成普通工具 permission:前者是 macOS 系统权限,后者是 Claude Code 的动作授权,两层都可能阻止执行。
Computer Use Input:实际外部副作用边界¶
computer-use-input.node 导出:
key/keys/typeText;moveMouse/mouseButton/mouseScroll/mouseLocation;getFrontmostAppInfo。
原模块保留 Enigo key mapping,包括 F1-F20、左右修饰键、媒体/亮度/照明、Launchpad、Mission Control 和 numpad。输入校验错误已用无副作用 probe 验证,例如空 keys、invalid key/action/button/axis;本轮还验证了 Accessibility NoPermission 文本和各 API 是先校验参数还是先初始化 Enigo。
这里的风险边界很清楚:schema/permission/hook 通过后,native call 会向 OS 注入真实输入,之后的 model fallback 或 transcript rewind不能撤销点击、按键和已触发的外部应用动作。自动测试默认只跑只读状态和校验错误,不主动发送真实键鼠事件。
Audio capture:线程、采样与授权¶
reverse/javascript/cli.readable.js 362436-362498 展示 audio bridge:
- 尝试 embedded/相对路径 module;
- module 不可用返回 null;
- forward
startRecording/stopRecording/isRecording; - forward playback 与
microphoneAuthorizationStatus; - 在 voice app state 中缓存 lazy-load Promise 和 module。
静态证据保留 napi-2.16.17、cpal-0.15.3、coreaudio-rs-0.11.3。这里必须把三层事实分开:原版 JS wrapper 只把 startRecording callback 收到的 bytes 原样上抛,本身不声明采样率转换(reverse/javascript/cli.readable.js 362451-362455);SoX fallback 明确使用 -r 16000 -e signed -b 16 -c 1(362559-362580),Voice WebSocket query 也声明 encoding=linear16、sample_rate=16000、channels=1,这是可观察的 fallback/wire 消费合同;原版 native 内部是否以及怎样完成 16 kHz、单声道、signed 16-bit 重采样,当前静态证据没有恢复。授权路径动态加载 AVFoundation 并调用 authorizationStatusForMediaType:。
音频状态机¶
lazy load module
-> query microphone authorization
-> startRecording(callback)
-> native callback emits audio bytes / SoX emits linear16 audio
-> JS forwards bytes into voice state and WebSocket queue
-> stopRecording
playback: startPlayback -> writePlaybackData* -> stopPlayback
原版 probe 证实初始 {recording:false, playing:false, mic:0},播放启动后 isPlaying=true,停止后立即 false。重建版为匹配 SoX/wire 消费合同独立实现 16 kHz/mono/s16 转换、静音阈值和内部 buffer policy;这些是 Derived / Compatible,测试通过也不能升级成原始 native 源码事实。
URL handler:Apple Event 与超时¶
JS wrapper 在 reverse/javascript/cli.readable.js 604122-604133 lazy load module;不可用返回 null,否则调用 waitForUrlEvent(timeout)。Mach-O import/反汇编显示 AEInstallEventHandler、AEGetParamPtr、GURL direct object 和 UTF-8 decode。
这条能力用于 macOS URL scheme/登录回调一类事件。原版与重建版 waitForUrlEvent(1) 在无事件时都返回 null。超时是正常结果,不应自动当成 module crash。
N-API 边界要检查什么¶
每个版本至少比较:
- 顶层 export 名;
- object tree 和 prototype method;
- sync/Promise/callback 形态;
- 参数数量、可选值、null/undefined 语义;
- 返回 object 字段;
- error type/message;
- resource ownership/dispose;
- thread-safe function 和 abort;
- 每个 architecture slice;
- linked framework/dependency version。
只比较 _napi_register_module_v1 或导出数量无法发现字段/生命周期漂移。
原版与重建版双跑¶
module-exports.json 固化 5 个模块的 function/object/prototype contract。build_and_validate.sh 会:
- 先运行 validate_computer_use_semantics.mjs,核对原版两个架构的 Finder 静态对象、
0.1alpha 常量和关键指令;它还会从两个原版 slice 的静态数组对象解码精确 8 项截图白名单,核对数组初始化、computeExcludedApps -> Set.contains消费链、full/region nil 分支、cstring 地址与 79/75 字节长度,再按函数绑定兼容 Swift 源码中的对应错误合同、systemChromeBundleIdsunion 和failureMessagecatch,并内置两项消费链故障注入; - 格式/编译 4 个 Rust crate 和 1 个 Swift package;
- 把产物复制到全新临时目录并改成原
.node文件名; - 分别加载 original 与 reconstructed;
- 比较 exports 和 23 组行为;
- 写出 arm64 native-reconstruction.json 与 x86_64 native-reconstruction-x86.json,逐项记录模块、比较模式、输入策略和架构证据;
- 避免覆盖已经映射的 Mach-O。
当前 arm64 结果:
computer-use static semantics: PASS
native contract validation: PASS
modules checked: 5
native behavior comparison: PASS
checks passed: 23
reconstructed native validation: PASS
通过说明重建实现满足当前检查过的外部合同,不说明内部算法等同原始源码。
发布脚本提供 Rust x86_64-apple-darwin target 和 Swift --triple x86_64-apple-macosx 两条 build recipe,再把准备好的 x86 artifact 交给 Rosetta x86 Node。下面是 x86_64/Rosetta artifact 合同与运行比较的输出;x86 报告没有绑定同次 Rust/Swift build 的 literal output 和 exit status,因此这段输出不能单独证明 artifact 在该次报告生成时由当前源码新鲜构建:
native contract validation: PASS
modules checked: 2
native contract validation: PASS
modules checked: 5
native behavior comparison: PASS
checks passed: 20
第一组 2 个合同验证原版确实存在 x86 slice 的 Computer Use 模块;第二组验证 5 个 supplied compatible artifact 都能加载且导出符合合同。20 项行为结果由 Input/Swift 的 19 项同输入对照和 1 项覆盖 guard 构成。报告把这一级写成 validated-artifacts-and-runtime,不是 build-and-runtime。
x86 报告还逐 artifact 保存 bytes、SHA-256、Mach-O slice 和 regular-file 状态;比较器自行执行导出合同,并拒绝 symlink、目录逃逸、缺失模块、非 x86 slice 与 original/rebuilt 同 hash。环境字段同时要求 x64 Node、arm64 hardware 和 sysctl.proc_translated=1,因此这里的 Rosetta 不是根据 process.arch 猜测。专属 forged-directory 测试证明拿 original symlink 冒充 compatible 不能生成 PASS 报告。
机器报告把 23 项拆为:22 项真实原版/重建对照和 1 个最低覆盖审计。22 项对照中有 14 exact、5 normalized-semantic、3 schema-and-invariants,本轮 environment-boundary 为 0。exact 用于稳定状态、错误和归序后的完整 hide 候选;normalized-semantic 用于应用、显示器和 icon 等先归一化再比较的结果;schema-and-invariants 用于输入模块和实时截图结构。hide 候选保留全部字段与成员:公共 helper 固定豁免 Finder,并筛掉非 layer 0、alpha 不大于 0.1 或不与目标显示器相交的窗口;它不检查窗口宽高,源码中 width/height > 1 只用于窗口所属显示器和普通激活候选两个独立流程。prepareDisplay 调用方又额外豁免 host 与 Finder,preview 调用方不另加 Finder,但仍经过公共 helper,并把可选 display ID 解析为指定或主显示器 frame。screenshot 则单独使用 8 项系统界面 bundle ID 白名单,loginwindow 只属于该集合。截图比较字段、尺寸、显示器元数据、规范 Base64 和 JPEG SOI/EOI,并由对应原版/重建版图像模块实际解码,要求格式为 JPEG 且解码宽高等于返回值;不比较实时像素字节。只有双方返回完全相同的已知 TCC/ScreenCaptureKit 失败合同才改记 environment-boundary,其他成功/失败组合或错误文本漂移都会失败。构建前置静态校验同时绑定 arm64/x86_64 的 Finder 编码、0.1 常量和关键指令。比较器先使旧报告失效,再把 PASS 或 FAIL 通过临时文件、fsync 和 rename 原子写入;因此失败运行不会把旧 PASS 留给后续 validator。这样后续版本能看出是“行为变了”“环境没提供能力”还是“总覆盖不足”,而不只是总 PASS 变成 FAIL。
架构与平台边界¶
发布产物中 computer-use-input.node 和 computer-use-swift.node 含 arm64 + x86_64 slice;其他模块的归档架构见 native-architectures.txt。compatible 源码有 arm64/x86 build recipe;提交的 x86 报告只对 supplied artifact 的架构、身份分离、load/export 和 Rosetta runtime 负责。
| 层 | arm64 | x86_64 |
|---|---|---|
| Original 发布模块 | 5 个模块,runtime + static | 2 个 Computer Use slice,runtime + static |
| Compatible 重建模块 | 5 个模块,build + runtime,23/23 checks | 5 个 supplied artifact 通过架构、非同 hash、load/export 验证;2 个可对照模块 20/20 checks |
因此“x86 重建通过”有两种强度:5 个 compatible artifact 都证明了 x86 架构、与 original 非同 hash、load/export;只有发布物自身含 x86 slice 的 Input/Swift 两个模块能进一步证明 original/compatible 同输入行为。其余三个模块没有 original x86 对象,不能从 compatible-only 成功外推原版行为;两条 build recipe 也不能替代同次构建输出与退出状态。
跨版本应分别比较 slice。universal module 的一个 slice 未变化,不代表另一个 slice 未变化;当前两份报告也必须分别保持 PASS,不能用任一架构替代另一份执行证据。
失败与恢复¶
| 失败 | JS 层应观察到 | 恢复方向 |
|---|---|---|
| module 缺失/加载失败 | null/false 或明确 unavailable error | 平台/安装检查,禁用 surface |
| N-API shape 漂移 | method undefined / argument error | 版本成套安装 |
| TCC 未授权 | check state/request failure | 用户在系统设置授权 |
| screenshot capture fail | rejected Promise/null | display/window/permission 诊断 |
| audio device fail | start false/error callback | 设备/采样/授权诊断 |
| input validation fail | 稳定参数错误 | 模型或 schema 修正 |
| native crash | CLI 进程退出 | crash report + exact module hash |
Native 操作已经改变外部状态时,Agent Loop 的 message tombstone、resume repair 和 fallback 不能撤销它。键鼠/应用打开需要幂等策略或人工确认。
微小但关键的特性¶
ImageProcessor是一次性消费对象,不是每次调用重新 decode。- screenshot base64 不带 Data URL prefix。
- installed apps 保留重复 bundle ID 和 Spotlight 顺序。
appUnderPoint结果不带 pid。- optional integer 的 null/undefined 都映射 Swift nil。
- URL timeout 返回 null,是正常无事件状态。
- audio module lazy-load 后缓存在 app state。
- module unavailable 的 JS 行为因模块而异:有的抛错,有的返回 null/false。
- 已加载
.node不能在原路径热替换后继续可靠测试。
证据与边界¶
结构化证据条目除静态 consumer 外,还包括 probe.native-original-compatible、probe.native-x86-build-load 与 probe.native-x86-original-compatible。完整 Mach-O 证据在 reverse/native,归一化 ABI/依赖/Swift symbol diff 在 reverse/index。
本仓库已经尽可能恢复发布物可观察合同和兼容源码;它不是 Anthropic 构建前的原始 C/C++/Rust/Swift 仓库。缺失源码级 debug info 后,内部文件布局、注释、优化前函数体和被编译器删除的代码不能逐字恢复。