机制解剖 · 实践复盘

REA 工作原理

编排、路由与证据:一个分析编排层的内部机制,以及它在零引擎环境下的真实边界

这篇文档不讲"REA 能做什么",讲"它为什么是这样运作的"。所有结论来自一次完整实践:安装 REA 6.4.0 → 读源码分层与依赖 → 用它逆向 /System/Applications/Stickies.app → 落到一份可运行的 HTML 单页应用。文中每条机制判断都挂命令输出或源码路径;另有三条我一度写进交付物、后来被实测推翻的判断,单列一章。

实测时间 2026-10-12 版本 rea-agents 6.4.0(npm 最新) 分析目标 Stickies.app(SIP 保护 · universal Mach-O) 引擎环境 零原生引擎 样本规模 2,957 文件 / 2,444 提交

00摘要:十二条核心论断

每条都是可证伪的机制判断,后面各章逐一展开。证据标记见表末说明。

36
运行时依赖
其中反编译引擎 0 个
18
零引擎可用操作
(target-free,全部可用)
170
MCP 工具
(CLI 侧 129 个子命令)
780
单次 nib 解码节点
421 条边
  1. REA 自己不产出分析知识。它把"知识"外包给 provider,自身只负责调度、约束与存档。证据:package.json 的 36 个运行时依赖里,没有任何原生反编译引擎;唯一带分析语义的依赖是 evmole(EVM 字节码,纯 WASM 可内联)。 源码
  2. 它的调度单位是 provider,不是工具。170 个 MCP 工具只是 provider 能力的投影面;引擎(Ghidra/Hopper/IDA/JEB)本身也只是 provider 的一种实现。证据:官方架构图把 13 个 provider 与 src/server/(仅做请求转译)分列为不同层。 源码
  3. provider 选择是确定性的,且失败即报错不猜测。注册表按排序取候选、命中即用、无模糊回退。实测对 Stickies.app 调 rea inspect,直接返回 provider_unavailable 并逐条列出四个引擎各自的不可用原因。 实测
  4. 能力面天然分两层,且可以在零引擎下完全运行。rea capabilities --json 返回 18 项 target-free 操作,本机零引擎环境下 18/18 全部 available。这条"纯净子集"是它可裁剪性的来源。 实测
  5. "操作可用"不等于"对任意目标可用"——这是两套不同的可用性。inspect_asset_catalog 在能力目录里是 available: true,但真拿 Assets.car 去调,返回 target_unavailable / unsupported binary format。能力声明管"操作存不存在",目标准入管"这个输入能不能读"。 实测
  6. 它用一张"产物图"统一所有目标类型。nib、JavaScript 应用、.NET 程序集、Mach-O、APK 都归一到 nodes + edges + kind 的同一结构。实测 decode-interface-builder 对 3 个 nib 产出 780 节点 / 421 边。 实测 源码
  7. 它真正可移植的资产是证据模型,不是分析算法。输出结构固定为 observations / hypotheses / contradictions / unexplored_branches / next_probes,每份证据带稳定 ID 与 authority 分级。换掉引擎,这套口径不变。 实测
  8. 复刻不是附带功能,是一等公民。仓库里有独立的义务台账服务、覆盖度服务、9 阶段一致性契约,以及 docs/reconstruction-obligation-ledgers.md 与 docs/reconstruction-readiness.md,并有 CI 脚本 verify-reconstruction-readiness.mjs。 源码
  9. 它的判定是 fail-closed:不合格一律不聚合为通过。契约明确规定 fail、unknown、unsupported、truncated、skipped 永不聚合为 pass;每阶段与每项检查必须恰好出现一次并链接到证据。 源码
  10. CLI 与 MCP 是同一内核的两个薄适配器,能力等价。官方架构图里 scripts/rea.mjs 只是 dispatcher,把请求路由到 src/cli.ts(one-shot)或 src/main.ts(MCP stdio),两者共用 src/application/ 与 src/composition/。所以未激活 MCP 不构成能力损失。 源码 实测
  11. 它的价值上限由你装的 provider 决定。这是"编排层"定位的必然代价——工具再多,反汇编深度仍取决于你装了 Ghidra 还是 Hopper。反过来也成立:不装引擎时它退化成一套诚实、可复现的静态产物分析器,而非什么都做不了的空壳。 推断
  12. 它把"分析失败"与"环境损坏"分开归因。9 阶段契约里 diagnose-environment 的必需证明是"把损坏的运行时与分析失败分开、检测版本偏差、恢复后 revision 变化"。多数工具会把两者混成一个"运行失败"。 源码

怎么读这篇文档

01—02 章讲静态形态(它由什么构成);03—05 章讲运行时机制(一次调用如何在内部流转、边界在哪);06—09 章讲数据契约(产物图、证据模型、双接口、复刻契约);10—12 章讲工程与用法(进程、失效模式、正确姿势)。第 11 章专门收录三条被实测推翻的判断——如果你只想看最有信息量的一章,看它。

01定位判定:它为什么不是反编译器

先把最容易误判的一点钉死,后面所有机制都建立在它之上。

第一次看到 REA 的能力清单,最自然的理解是"一个 AI 版的反编译器"。但这个理解会立刻导致一连串错误的预期:为什么装完它还是"什么都反不了"?为什么 doctor 退出码是 1?为什么 170 个工具里绝大多数在零引擎环境下用不了?

正确答案是:REA 是分析编排层(analysis orchestration layer),不是分析引擎。它把"产生分析知识"这件事整体外包给 provider,自己只做三件事——把请求路由到合适的 provider、把能力边界与限制声明清楚、把观测结果封成可复现的证据。

证据一:依赖审计——36 个运行时依赖,零引擎

这是最直接的判据。一个真正的反编译器必须依赖反编译算法;如果依赖树里一个都没有,它在结构上就不可能是反编译器。

0 2 4 6 结构化文本解析 7 通用工具 7 终端与进程 6 Schema 与校验 4 配置与清单 3 归档与打包 2 浏览器与网络 2 MCP 协议 2 日志与诊断 1 版本控制 1 字节码分析 1 · evmole
图 1 · REA 的 36 个运行时依赖按职责分类。整棵依赖树里没有任何原生反编译引擎:没有 Ghidra 绑定、没有 Capstone/LLVM、没有 IDA SDK。唯一带分析语义的是琥珀色的 evmole——EVM 字节码反编译器,且是纯 WebAssembly 实现,可内联进进程而不需要外部可执行文件。这个细节恰好体现了架构偏好:能内联的就内联,需要外部引擎的一律交给用户自备。(数据来源:package.json dependencies 字段,36 项全列)
表 1 · 36 个运行时依赖的完整归属(实测取自 package.json)
职责类别数量代表包与"分析能力"的关系
结构化文本解析7@babel/parser, @babel/types, parse5, @xmldom/xmldom, jsonc-parser, lossless-json, smol-toml把各种源码/标记/BLOB 解析成 AST 或 DOM。是解析器,不是反编译器——只做语法还原,不做语义恢复
通用工具7@clack/prompts, canonicalize, diff, ignore, incur, write-file-atomic, @jridgewell/trace-mappingCLI 交互、确定性序列化、结果比对、原子落盘、源码映射
终端与进程6@lydell/node-pty, @xterm/headless, @xterm/addon-serialize, cross-spawn, stream-chain, stream-json为"自有子进程"提供终端仿真与结构化流——是运行被测程序的基础设施
Schema 与校验4ajv, ajv-formats, zod, semver输入契约与版本约束的运行时校验,是"确定性"的前提
配置与清单3yaml, plist, har-schema读客户端配置、Apple plist、HTTP 归档
归档与打包2@zip.js/zip.js, @electron/asar解包 zip / ASAR,属产物层
浏览器与网络2playwright-core, wsCDP/Playwright 驱动真实浏览器做动态观测
MCP 协议2@modelcontextprotocol/client, @modelcontextprotocol/server对外接口层的 SDK
日志与诊断1pino结构化日志
版本控制1isomorphic-git纯 JS 的 git 客户端——用于分发 bundled skill
字节码分析1evmole全树唯一的分析型引擎,且限定在 EVM 域、以 WASM 形式内联

证据二:官方架构图把"引擎"和"调度"分到不同层

仓库的 docs/architecture.mermaid 用 11 个 subgraph 描述了自己的构成。其中 src/server/ 的标注是 「MCP request translation」(MCP 请求转译),而所有引擎——Ghidra、Hopper、IDA、JEB、pwntools、EVM、Binwalk——都落在独立的 Provider Layers 里,并且各自的标注都在强调适配而非实现:

Provider Layers
├─ src/ghidra/   Ghidra discovery, profile & function analysis
│                snapshot, Unix/Windows transport, bounded lifecycle
├─ src/hopper/   Hopper launch & Unix-socket protocol
│                BridgeLauncher, HopperClient, protocol
├─ src/ida/      Upstream MCP contract adaptation
│                live GUI binding or owned headless database
├─ src/native/pwntools/ + pwndbg/
│                BYO offline ELF/core adapters
│                unchanged upstream parsers, ABI structures
├─ src/evm/      unchanged EVMole/WASM in bounded owned worker
└─ ... (共 13 个)

注意几个措辞:「adaptation」(适配)、「BYO」(bring your own)、「unchanged upstream」(上游未改动)。REA 在架构文档里明确承认这些能力不是它实现的:src/ida/ 是"上游 MCP 契约的适配",pwntools/pwndbg 是"BYO 离线适配器,上游解析器/ABI 结构/映射 API 均未改动"。

判据

如果一个项目自认它的核心分析组件是"适配"和"未改动的上游",那么它的原创性必然在别处。REA 的原创性在三处:provider 路由、能力边界声明、证据模型。后面 03—07 章就是逐个拆这三样。

证据三:实测报错自证了边界

最干脆的证据来自一次真实调用。用 REA 的"正规入口"分析一个 macOS 应用:

$ rea inspect /System/Applications/Stickies.app
error: provider_unavailable
  operation = current_document
  ghidra : not_configured
  ghidra : architecture_unsupported   ← v1 无法强制 universal Mach-O 的架构
  hopper : executable_missing
  ida    : not_configured

它没有尝试"降级用别的方式凑一个结果",没有返回半个分析,也没有含糊地报"分析失败"。它做的是:列出所有候选 provider,逐个说明为什么用不了。这正是"编排层"的自证——当没有可用的执行者时,编排者唯一诚实的动作就是报告"没有执行者"。

反过来:它也不是"空壳"

把 REA 简化成"只是个包装层"同样是误判。零引擎环境下它依然能完成实质工作——18 项 target-free 操作中有 decode_interface_builder(解 nib)、inspect_keyed_archive(解 keyed archive)、inspect_plist、inspect_macho、list_architectures、demangle_swift、trace_dylib_resolution 等。这些不是"包装",是它自己实现的静态解析能力(src/domain/apple/interfaceBuilderGraph.ts、src/domain/artifactGraph.ts 等)。

所以准确的定位是:REA = 自研的静态产物分析内核 + 对第三方引擎的统一编排 + 一套证据契约。三者权重随你安装了什么而变。

它不是

  • 不是反编译器——依赖树里零引擎,核心能力靠外部 provider
  • 不是分析算法库——不实现反汇编/反编译/符号恢复算法
  • 不是规则引擎 / 特征库——没有静态规则集,判定来自证据而非模式匹配
  • 不是一次性脚本集合——有会话、身份、账本、缓存等状态设施

它是

  • 是调度层——把请求按 target 与 operation 路由到唯一合适的 provider
  • 是约束层——每项能力都声明自己的可见限制(exact visible limits)
  • 是存档层——把观测封成带稳定 ID 与 authority 的可复现证据
  • 是编排层——把多个 provider 的产出组织成可评估的完整调查

02分层架构:依赖为什么必须向内流动

官方架构图给出 11 个层次。理解层间依赖方向,才能理解为什么"换个引擎"不会牵动其他部分。

仓库的 docs/architecture.mermaid 是理解 REA 结构最好的入口。它把系统切成 11 个 subgraph,并且给出了层间连线。把连线关系提炼出来,依赖方向非常干净:

DISPATCH (scripts/rea.mjs)  →  CLI (src/cli.ts)  |  MAIN (src/main.ts)
MAIN  →  CONFIG  |  SERVER  |  APP
CLI   →  APP
MAIN  →  COMPOSITION     CLI → COMPOSITION     SERVER → COMPOSITION

关键点在于 Composition 层被三个入口同时依赖,而 Composition 自己不依赖任何入口。这是典型的依赖倒置:入口层薄,装配层重,能力层(provider)只被装配层引用。

入口层 scripts/rea.mjs · src/cli.ts · src/main.ts 组合层 src/composition/ typed provider & session factories 适配器层 src/server/ · hopper_bridge.py · ReaGhidraBridge.java 应用层 src/application/ Router · Registry · EvidenceLedger Provider 层 src/ghidra/ hopper/ ida/ jeb/ browser/ dotnet/ … 共 13 个 进程基础层 src/process/ owned groups · private roots · deadlines 契约层 src/contracts/ caller-visible schemas 领域层 src/domain/ pure side-effect-free modules 依赖向内
图 2 · REA 的八层结构(另有三层未收入:配置层 src/config/ 为横切关注点、测试 tests/、评估 src/evaluation/)。层号自上而下递增,依赖只向内流动:入口层依赖应用层,应用层依赖契约与领域层,领域层不依赖任何其它层。provider 层不在任何入口的直接依赖路径上——它只被组合层装配。这正是"换引擎不牵动其它部分"的结构保证。(数据来源:docs/architecture.mermaid)

三件关键装置

官方架构图在应用层单独点名了三个装置。它们是理解 REA 运行时行为的钥匙:

SessionProviderRouter
+ BinarySession
每个目标一个不可变深绑定。一旦选定 provider 处理某个二进制目标,整条会话链路都绑定在它上面,中途不切换。标注还提到「disjoint auxiliary routing」——辅助能力(如符号 demangle)走独立的、不相交的路由,不污染主绑定。
AnalysisProviderRegistry
排序的深候选 + 确定性选择、无回退。候选按序排列,选中即定,不因失败而模糊降级。这就是 provider_unavailable 里那份"逐条列出为什么用不了"的候选清单的来源。
EvidenceLedger
+ Unknown ownership
证据账本 + 未知归属。仓库里有 EvidenceLedger.residualUnknown.test.ts——"残留未知"是被单独测试的一等概念,而不是遗漏。这与第 07 章的失败哲学直接相关。

Provider 层的真实规模

官方图列了 13 个 provider subgraph。按文件数看它们的权重差异很大——这个分布本身就是一种表态:

表 2 · Provider 目录规模(按 git tree 递归统计的文件数,实测)
Provider文件数性质引擎依赖
src/browser/113CDP / Playwright 动态观测内联 需真实浏览器
src/ghidra/60反汇编 / 伪代码 / 引用 / CFG外部 Ghidra 自备
src/process/59自有进程组、私有根、时限无 基础层
src/artifacts/58产物库存与抽取、格式读取自研 无需外部
src/native/55pwntools/pwndbg BYO 适配外部 上游未改动
src/dotnet/34.NET 静态元数据(PE/CLI)自研 静态解析
src/hopper/29Hopper 启动 + Unix socket 协议外部 Hopper 自备
src/ida/23上游 ida-pro-mcp 契约适配外部 上游生态
src/javascript/23sourceMap 编解码、堆限制进程自研 固定上游 codec
src/firmware/10Binwalk / Unblob 适配外部 自带进程
src/evm/10EVMole/WASM 受限 worker内联 WASM
src/android/8静态 APK 适配、pin 住 JADX外部 再分发
src/inspector/8被动 V8 Inspector 发现与捕获内联 运行时
src/reference/8参考源文件读取无 纯 IO
src/jeb/5JEB 适配外部 商业许可

一个值得注意的分布

四个"外部引擎"provider(ghidra 60 / hopper 29 / ida 23 / jeb 5)合计 117 个文件,而对应的"自研产物分析"能力(artifacts 58 + dotnet 34 + javascript 23 + android 8 + inspector 8)合计 131 个文件。它花在"自己解析产物"上的代码量,并不少于花在"适配别人引擎"上的。这与"只是个包装层"的印象正好相反。

另注意 Ghidra(60)远超 Hopper(29)与 JEB(5)——因为 Ghidra 开源、可 headless 自动化,而 Hopper/JEB 是商业软件、需要图形界面或商业许可。provider 的权重分布,实际上由"可自动化程度"决定,而不是由"引擎强弱"决定。

03一次分析的完整生命周期

从请求进入到证据封存,中间发生八件事。其中"run 分配先于 provider 启动"是最容易被忽略、却最能说明设计意图的一步。

把 CLI 与 MCP 两条入口合起来看,一次分析请求的流转可以切成八段。这段流程不是我从文档猜的——它由三处交叉确认:官方架构图的层间连线、docs/mcp-contracts.md 里对 binary_session 的逐字段规定,以及实测的 provider_unavailable 报错结构。

01 请求进入 CLI one-shot / MCP stdio 02 会话身份 binary_session 协商 03 目标解析 二进制 · 架构 · 配置文件 04 Provider 选择 确定性选取 · 无回退 05 run 分配 run_id 先于启动 06 能力裁剪 按组给出可用性 07 执行与观测 自有进程 · lineage 08 证据封存 evidence_id · authority 失败不回退:选定 provider 运行失败后永不被自动替换 fail-closed —— unknown 与 fail 显式保留,永不聚合为 pass
图 3 · 一次分析请求的八段流转。第 04 段(provider 选择)与第 05 段(run 分配)的先后顺序是契约明文规定的:每次成功的目标切换都必须在任何 provider 启动之前分配 analysis_run.run_id。红色部分标出失败路径的关键性质——它不尝试替代方案,而是把失败类型化后如实返回。(依据:docs/mcp-contracts.md、docs/architecture.mermaid、实测 provider_unavailable 输出)

逐段说明

表 3 · 八段的职责与契约依据
段名称发生什么依据
01请求进入scripts/rea.mjs 作为 dispatcher 把请求路由到 CLI(one-shot)或 MCP stdio server。两条路径共用同一个内核。architecture.mermaid
02会话身份binary_session 报告活动 package、server、SDK 与协商后的协议细节。这是后续一切权限与能力判定的基线。可选传入 expected_package_version / expected_catalog_digest / expected_server_path 与调用方预期做比对。mcp-contracts.md
03目标解析解析出实际二进制(对 .app 而言是 Contents/MacOS/ 下的可执行文件)与架构信息。BinaryTargetResolver / AppBundleExecutable 负责这一层。src/application/
04Provider 选择先做 target-free 发现(按 provider ID 排序、报告 host 可用性与 unknown 目标支持情况、不创建 analysis client),再按确定性策略选取。选中即绑定,见第 04 章。mcp-contracts.md
05run 分配分配 analysis_run.run_id。这一步必须先于 provider 启动——保证任何后续失败都能归因到一次可标识的运行,而不是"某个说不清的进程崩了"。mcp-contracts.md
06能力裁剪按组给出当前可调用操作,每组携带 surface、availability、reason、remediation、default-mode 状态、modes 与 client 需求。mcp-contracts.md
07执行与观测provider 在自己的进程中执行。process_lineage 在动态 provider 启动前是 not_observed,启动后转为 snapshots,逐条记录 launcher PID、父 PID、进程组与后代;无法重新验证所有权时记为 unavailable 并附原因。mcp-contracts.md
08证据封存观测归一化为 normalized_result,写入 EvidenceLedger,分配稳定 evidence_id 与 authority。详见第 07 章。实测 + src/domain/evidence.ts

第 05 段为什么值得单独说

把 run_id 的分配放在 provider 启动之前,是一个很小的实现决策,但它决定了三件事:① 失败的归因粒度——是"这次 run 没成"而不是"进程挂了";② 重放能力——有了稳定 run 标识,同一次调查才可以被确定性重放;③ 所有权审计——process_lineage 的每条观测挂在 run 上,进程泄漏才有归属可查。

配套的是第 07 段的 verified / unavailable 二态:它不把"没能验证所有权"当成"没有所有权",而是标成 unavailable 并说明原因。这与第 09 章的 fail-closed 是同一种价值观。

一个反向证据:目录是不变的

契约文档里有一条容易被读漏、但很能说明问题:

tools/list returns the complete canonical tool inventory,
including tools that are currently unavailable.

Opening or closing a target or observing a provider health
transition leaves that catalog unchanged and does not emit
notifications/tools/list_changed.

也就是说:打开一个引擎、或引擎从故障中恢复,都不会让工具目录发生变化。目录是"规范全量",不是"当前可用集"。这与很多工具"动态注册能力"的做法正好相反。

这个选择的代价是:调用方必须在每次调用前用 binary_session 查当前可调用集,不能靠目录推断。收益是:宿主(以及模型上下文)里的工具定义永远稳定——不会因为某个引擎的启动或崩溃而让上下文里的一大块 schema 失效重算。

这就是为什么"170 个工具"和"18 项可用"可以同时成立

实测 tools/list 返回 170 个工具(170/170 带完整 inputSchema),而 rea capabilities --json 在零引擎环境下只承认 18 项可用。这两个数字并不矛盾——前者是规范目录(不随环境变),后者是当前路由的可用集(随 host、target、provider、客户端能力变)。契约文档把这两者的关系写得很直白:判断工具是否真的可用,应当以 binary_session 的 tool_availability 为准,tools/list 只负责提供完整定义。

04Provider 模型:确定性与不可替换性

"确定性选择、无回退"听着像一句广告语,但它有精确的字段级语义。这一章把它拆开。

REA 的 provider 不是插件式的"多路尝试"。它是一套带类型的选择协议——候选、绑定、选择来源、歧义,各自是不同字段,语义不可互换。契约文档对此有明文规定。

三类 provider

13 个 provider 按"能力从哪来"可以归为三类。这个分类决定了它们在零引擎环境下的命运:

自研静态解析

src/artifacts/ src/dotnet/ src/javascript/ src/android/ src/inspector/ src/domain/apple/

零外部依赖。nib 图解析、keyed archive、plist、Mach-O 头部、.NET 元数据、APK 资源、sourceMap 解码,都是 REA 自己实现的解析器。这是它在零引擎环境下仍能做实质工作的原因。

外部引擎适配

src/ghidra/ src/hopper/ src/ida/ src/jeb/ src/native/ src/firmware/

BYO(自带)。REA 提供启动、协议、快照、生命周期管理,引擎本体由用户自备。Ghidra 走打包的 HeadlessScript(ReaGhidraBridge.java),Hopper 走进程 + Unix socket 协议(hopper_bridge.py)。

运行时观测

src/browser/ src/inspector/ src/process/

需要真实运行时。CDP/Playwright 驱动浏览器;V8 Inspector 被动捕获脚本与目标位置;进程捕获记录真实执行。这类 provider 产出的是"动态证据",与静态证据在 authority 上不等价。

选择协议的精确语义

契约文档对 binary_session 的几个字段做了明确的区分,这几条合起来就是"确定性"的完整定义:

analysis_provider_candidates
深度引擎发现的权威来源。每个候选的能力集描述"这个可能的 provider 能做什么"——它们不构成选择。这是一个纯粹的发现结果,不产生副作用(不创建 analysis client)。
capabilities
(顶层)
描述当前路由。即"现在这条会话走的是谁"。
analysis_provider_binding
标识具体选定的路由——一个不可变的 provider、具体版本、选择来源、完整分析画像。契约原文特别强调这几个字段不可互换(not interchangeable)。
open_binary.provider_id
接受具体 provider ID 或 auto;使用与 CLI --provider 和 REA_ANALYSIS_PROVIDER 同一套解析与选择策略。这是 CLI/MCP 能力等价的一个具体证据。
歧义与失败
歧义、未知、不可用、不支持的选择各自返回带类型的 selection details——不兜底、不猜。
不可替换性
"A selected provider is never replaced automatically after a runtime failure." 选定之后,即使运行时失败,也不会自动换一个 provider 顶上。
深开请求(analyze / decompile) 候选发现 按 provider ID 排序 · 不创建 analysis client 逐候选检查 已配置? 可执行? 架构受支持? 歧义 / 未知 返回带类型的 selection details 零命中 provider_unavailable + 逐条不可用原因 唯一命中 → 绑定 不可变 provider 版本 · 来源 · 分析画像 绑定后运行时失败也不自动替换:run 失败 → 类型化错误,不换 provider 重试
图 4 · provider 路由的三条出口。左边两条是失败出口,各自携带不同粒度的信息:provider_unavailable 给的是"每个候选为什么不行"的清单,而歧义/未知给的是"选择过程本身不唯一"的类型化细节。右边是成功出口,一旦绑定就产生一个不可变的路由标识。(依据:docs/mcp-contracts.md 对 binary_session 的字段规定)

实测:失败出口长什么样

对 Stickies.app 调 rea inspect,命中的是中间那条出口。它的输出结构值得逐行读:

error: provider_unavailable
  operation = current_document      ← 失败的是哪个"操作",不是哪个"工具"
  ghidra : not_configured           ← 候选 1:装了没配
  ghidra : architecture_unsupported ← 候选 1 的第二个卡点
  hopper : executable_missing       ← 候选 2:可执行文件不在
  ida    : not_configured           ← 候选 3:未配置
  (jeb 甚至没进入候选)

三点值得注意:

这条实测推翻了我先前的一个判断

我在复刻规划里写过"接 Ghidra 只需设 GHIDRA_INSTALL_DIR"。实测不成立。Stickies.app 是 x86_64 + arm64e 的 universal Mach-O,而这里明确报 architecture_unsupported——装了 Ghidra 之后还得先把二进制 thin 成单架构,才能进入分析。我把这条列进第 11 章的更正清单。

05引擎边界:两套可用性,别混为一谈

"这个操作能用吗"和"这个操作能处理这个文件吗"是两个不同的问题。REA 把它们分成了两套机制。

零引擎环境下,rea capabilities --json 返回 18 项 available: true 的操作。这份清单是本次实践最有用的产出之一——它精确划出了"不装任何东西也能做什么"的边界。

target-free 操作 18 / 18 可用 完全不依赖外部引擎 —— 零引擎环境下的实际能力面 引擎候选 ghidra not_configured · architecture_unsupported hopper executable_missing ida not_configured jeb 未进入候选集(商业许可,未安装) 零引擎可用的 18 项操作全部标 available:true 而任何需要反汇编 / 伪代码的操作都落在引擎候选上 两者互不重叠:不装引擎不会让 18 项里的任何一项失效
图 5 · 零引擎环境下的能力边界。上半部分 18 项 target-free 操作全部可用;下半部分四个引擎候选全部不可用,且各自给出了不同的原因码。两栏之间没有交集——这意味着"装引擎"只会新增能力,不会修复或影响已有能力。这正是分层解耦的直接后果。(数据来源:rea capabilities --json 与 rea inspect 实测输出)

18 项零引擎能力全清单

按作用对象分组,这 18 项构成了一套完整的"静态产物分析"工具集:

表 4 · 零引擎可用的 18 项操作(rea capabilities --json 实测,18/18 available: true)
分组操作项作用
Apple / 产物 decode_interface_builder11 解析 nib/xib 成产物图(本次实践的主力)
inspect_keyed_archive解析 NSKeyedArchiver 归档
inspect_asset_catalog读取 Assets.car 元数据
inspect_machoMach-O 头部与加载命令
inspect_plist属性列表解析
inspect_signature代码签名与授权信息
list_architectures列出 fat binary 中的架构切片
demangle_swiftSwift 符号反混淆(不需要 Hopper)
trace_dylib_resolution逐条追踪动态库解析路径
extract_artifact从包中抽取子产物
inspect_artifact产物库存 + 后续探测建议
托管代码 inspect_managed_artifact3 .NET / CLI 程序集元数据
inspect_managed_members类型与成员清单
inspect_managed_native_boundaries托管到原生的边界
运行观测 capture_native_ui_scenario4 观测原生 UI 交互场景(本次未跑,见第 11 章)
observe_native_ui原生窗口/控件观测
observe_native_calls原生调用序列观测
inspect_native_dispatch_metadataObjective-C 消息分发元数据

第二套可用性:目标准入

上表里有一项 inspect_asset_catalog。它在能力目录里是绿的。但真拿一个 Assets.car 去调用时:

$ rea inspect-asset-catalog .../Stickies.app/Contents/Resources/Assets.car
error: target_unavailable
  reason: unsupported binary format

这不是 bug,也不是配置问题。它是两套独立机制在各自如实表态:

第一套:操作级可用性

回答"这个操作在这个 host 上能不能被调用"。取决于:是否装了所需引擎、客户端是否提供所需能力、操作是否处于默认启用的 mode。

查询方式:binary_session.tool_availability 或 rea capabilities。

第二套:目标准入

回答"这个具体输入能不能被这个操作读"。取决于:文件格式、架构、版本、是否加密、是否被截断。

失败形态:target_unavailable + 具体原因(如 unsupported binary format)。

这条区分为什么值得单独讲

多数工具把这两种失败混成一个"运行出错"。混同的代价是无法区分"我缺个东西"和"这个文件我读不了"——前者你可以去装,后者你只能换方法。REA 把它们分开之后,第 09 章的义务台账才可能做到"给不出结果时明确告诉你缺什么、下一步该采什么证据"(required_next_evidence 字段)。

顺带纠正我自己先前的判断:我在复刻规划的自我批评里写过"Assets.car 是完全跳过的遗漏"。实测证明这是工具边界,不是我的疏漏——REA 自己也打不开这个文件。我已把这条列进第 11 章的更正清单。

三个状态,而不是两个

从 binary_session 与 rea doctor 的字段设计可以看出,REA 的可用性是三态的:

状态含义典型触发调用方该做什么
available可以调用所需条件全部满足直接调用
unavailable不可用,且原因已知not_configured / executable_missing / architecture_unsupported按 remediation 字段处置,或换操作
unknown不可用,且原因未知未建立活动连接时 doctor 对实时服务器状态的判定先建连接再判断,不要假定为可用或不可用

第三态最容易被忽略,也最能说明这套设计的性格。契约文档里对 rea doctor 的原文是:它"keeps live-server state unknown unless the active connection supplies identity"——在没有活动连接提供身份之前,它宁可说"我不知道",也不猜。

同样地,target-free 发现阶段会返回 unknown target support,而不是把"没试过"标成"不支持"。这是一个很克制、也很少见的默认值选择。

06产物图:一张图管住所有目标类型

nib、JavaScript 应用、.NET 程序集、Mach-O、APK——它们的分析结果落在同一个数据结构里。这是 REA 能"一套工具面覆盖多种目标"的结构前提。

实测中信息量最大的一次调用是 decode-interface-builder。它把 3 个 nib 文件解成了一张 780 节点、421 条边的图,并在结果里标了 hierarchy_complete: true。但真正值得注意的是这个结果的形状——它不是"nib 专用输出",而是通用产物图。

kind = view 视图节点 kind = control 控件节点 kind = outlet 出口声明 kind = objc_selector 方法选择器 kind = action 动作连线 kind = constraint 约束关系 同一套 nodes + edges 结构,也用于 JavaScript 应用图 · .NET 托管图 · 应用版本比较图 实测:单次 decode-interface-builder 产出 780 节点 / 421 边 / hierarchy_complete = true
图 6 · 产物图的节点类型。上半两类(view / control / outlet)表达结构,下半三类(objc_selector / action / constraint)表达行为与关系。本次实践中,正是这后三类让功能面得以还原——从图里直接读出了 80 个方法选择器与 11 个 outlet,而不是靠界面文案反推。(数据来源:rea decode-interface-builder JSON 输出的 normalized_result.graph)

节点与边的字段

从实测输出的 JSON 结构可以直接读出图的模型:

node.id
全局唯一标识,路径形式,包含所属文档(如 …/SNDocument.nib/…)。因此一个节点天然知道它来自哪个文件。
node.kind
节点类型枚举:view、control、resource、constraint、objc_selector、action、outlet。
node.name
可读名称(类名、方法名等)。
node.attributes.
interface_builder_object_id
回指 Interface Builder 内部的 objectID——保留了与原始工具的对应关系,使结论可回溯到具体控件。
node.attributes.nibValues
归档中的原始键值(如 NSSelectorName、NSMenuItems)。这是行为信息的来源——方法名就藏在这里。
edges[]
节点间的连接。本次实测 421 条。

同构的姊妹实现

产物图不是只有 Apple 一种。仓库里能数出一组平行实现,共用同一套"节点 + 边"抽象:

表 5 · 产物图模型的平行实现(源码路径实测)
领域实现作用对象同构点
src/domain/apple/
interfaceBuilderGraph.ts
nib / xib节点 = 对象 + 连线
src/domain/artifactGraph.ts通用产物包节点 = 产物内的可寻址单元
src/domain/javascript/
javascriptApplicationGraph.ts
JavaScript 应用节点 = 模块 / 入口 / 依赖
src/domain/javascript/
javascriptRuntimeReconciliationGraph.ts
静态图 × 运行时两个图的差分——静态声明与运行事实的对照
src/domain/managed/
managedApplicationGraphCoverage.ts
.NET 程序集节点 = 类型 / 成员 / 边界
src/domain/analysisView/
analysisViewCoverage.ts
分析视图覆盖度是图的一个投影

注意第四行:javascriptRuntimeReconciliationGraph——把静态图与运行时观测图做对账。这说明图模型不只是"记录",还是"比较"的载体。这直接支撑了第 09 章的义务闭合逻辑(静态事实只能产生候选义务,不能证明运行时行为)。

一个必须说清的边界

nib 解码的 limitations 里明确写着:不含像素几何。我实测确认了这一点——遍历全部 780 个节点的 attributes,frame / bounds / width / height / autoresizing 等几何键命中为零。

这意味着它可以告诉你"这个窗口有标题栏、有关闭按钮、有文本视图,关闭按钮触发了 performClose:",但不能告诉你这些控件的坐标和尺寸。要拿到几何,必须走 capture_native_ui_scenario 这类运行时观测 provider(见第 11 章)——也就是从"读归档"升级到"看运行"。

这个边界不是缺陷,而是权威等级的体现:静态图的 authority 不足以支撑几何结论,所以它不声称能给出几何。

07证据模型:五元口径与稳定标识

这是 REA 最可移植的一层,也是它和"跑个脚本输出一段文字"之间最本质的区别。

实测 rea inspect-artifact 的输出是一份 426 KB 的 JSON,其顶层结构给出了 REA 对"一次调查"的定义:

observations 直接观测到 的事实 hypotheses 由观测推出的 假设 contradictions 互相矛盾的 证据 unexplored_branches 未探索的 分支 next_probes 建议的下一步 (带 authority) 封存为带稳定标识的证据记录 evidence_id ev_ + 64 位 内容寻址 inspection_id ai_ + 64 位 一次调查 run_id 运行标识 先于 provider 启动 sha256 产物摘要 锚定到字节 catalog_digest 目录摘要 锚定到工具面
图 7 · 上半:一次调查的输出被拆成五个语义桶——观测、假设、矛盾、未探索分支、下一步探测。注意"矛盾"和"未探索分支"都是输出的正式字段,不是异常路径。下半:五类稳定标识,分别锚定"这条证据""这次调查""这次运行""这个字节流""这个工具面"。(数据来源:实测 rea inspect-artifact 输出结构、docs/mcp-contracts.md)

实测的标识长什么样

这些 ID 不是示意,是本次实践真实返回的值:

evidence_id    = ev_68273f72…
inspection_id  = ai_75fdbdff4d78311f95e2f7355a84e9be4249907362b59373bbbaa6efc

next_probes[0] = { operation: analyze_javascript_application,
                   authority: ast-static-analysis }

三个细节:

authority 是什么,为什么它重要

从契约文档能确认 authority 至少有三个取值:process、packaged-process、ast-static-analysis。它的作用在第 09 章的复刻台账里说得最清楚:

一个必需义务只有在"验证者的权威性与原始观测相当"(a passing verifier whose authority is comparable to the original observation)时才能闭合。

换句话说:你不能用一个静态分析的结论,去证明一个只有真实运行时才能确立的事实。这条规则把"证据强度"从一句定性描述,变成了机器可校验的字段约束。

这一层为什么是可移植资产

换掉引擎、换掉目标类型、换掉客户端——这套口径都不用改。观测/假设/矛盾/未探索/下一步这五个桶,对 nib 成立,对 JavaScript 应用成立,对 .NET 程序集也成立。所以 REA 的真正沉淀不是"支持了 13 种目标",而是"把这 13 种目标的分析输出,约束到同一套可评估、可比较、可复现的形态上"。

这也是它和"一堆分析脚本"的分水岭:脚本产出结论,REA 产出可被第三方复核的记录。

一条很讲成本意识的工程约束

契约文档里有这么一条,值得单独摘出来:

Hosts forward input schemas to the model with every request,
so advertised examples never copy complete producer Evidence.
An embedded Evidence record is shown as its exact
{"kind":"retained-evidence","evidence_id":"ev_..."} reference.

因为宿主(IDE、CLI 客户端)会把工具定义连同示例随每一次请求发给模型,所以工具广告里的示例绝不复制完整的证据正文——只给一个引用指针。这是对 token 成本的正向设计:工具面的大小不能随证据体量膨胀。

配套的还有 REA_MCP_INPUT_SCHEMA_PROFILE=compact:某些函数调用型客户端对单个工具的 parameters schema 有字面序列化大小上限(文档记录了 Moonshot 系客户端如 kimi-cli 在默认目录下每个请求都会失败,社区实测 13.7 KB 被接受、约 15 KB 被拒绝)。compact profile 把每个输入 schema 压进 13,312 字节预算,而服务端仍按完整 schema 校验——所以按精简广告调用的调用方会收到类型化错误,而不是被静默接受。

这个取舍很有意思:宁可让不按规范调用的客户端收到明确报错,也不放宽真实校验。又是一次 fail-closed。

08双接口:同一内核的两个薄适配器

"你没激活 MCP 就等于没用上 REA"是一个自然但错误的推断。这一章用结构证据说明为什么。

本次实践里,mcp__rea__* 工具始终未出现(需要用户在连接器页手动信任)。但整个工作流照常跑通了——这说明 CLI 与 MCP 不是"主从"关系。官方架构图给出了结构上的解释:

DISPATCH  scripts/rea.mjs
  ├─→  CLI   src/cli.ts + src/cli/
  │         One-shot CLI adapter
  │         setup, diagnostics, analysis, decompilation
  └─→  MAIN  src/main.ts + src/main/
            MCP stdio server adapter
            config, session, process lifetime

CLI  →  APP          MAIN  →  APP
CLI  →  COMPOSITION  MAIN  →  COMPOSITION
SERVER (MCP 转译) →  COMPOSITION

两条路径汇合在同一个 src/application/ 与 src/composition/。scripts/rea.mjs 只是一个 dispatcher(文档原话:Executable dispatcher,routes mcp / one-shot)。所以适配层是薄的,内核是共享的。

CLI 子命令(kebab-case) MCP 工具名(snake_case) decode-interface-builder decode_interface_builder build-reconstruction-obligation-ledger build_reconstruction_obligation_ledger capture-native-ui-scenario capture_native_ui_scenario compare-source-to-bundle compare_source_to_bundle evidence-export evidence_export analyze-javascript-application analyze_javascript_application
图 8 · 两个接口面的命名映射。左侧是 CLI 子命令,右侧是 MCP 工具名,双向箭头表示它们是同一个内核操作的两个暴露面。命名规则严格一致:kebab-case ↔ snake_case,逐词对应。(数据来源:rea --help 实测、MCP 工具清单实测、docs/mcp-contracts.md)

能力等价的三个结构证据

共享组合层
CLI 与 MCP server 同时依赖 src/composition/——provider 与 session 工厂是共享的,不存在"某条路径少一个 provider"的可能。
共享选择策略
open_binary.provider_id、CLI --provider、环境变量 REA_ANALYSIS_PROVIDER 使用同一套解析与选择策略(契约原文:it uses the same parser and selection policy as CLI)。
共享目录身份
CLI 的 capabilities 与 providers 返回与 MCP server_identity.catalog 相同的目录身份摘要。两边的"自我描述"是同一份。

实测:MCP 通道确实在工作

虽然会话里没有 mcp__rea__* 工具,但 MCP 服务器本身是通的。用事件驱动的 stdio 探针实测:

→ initialize (protocolVersion 2024-11-05)
← serverInfo {"name":"rea","version":"6.4.0"}
→ notifications/initialized
→ tools/list
← 170 个工具,170/170 带完整 inputSchema

同时实测出一个对使用者有实际影响的特性:冷启动成本高。首次 tools/list 需要加载全部 170 个工具的 schema,用固定延时等待的探针连续三次超时(最长等到 260 秒)。换成事件驱动——一收到 initialize 响应就立刻发 tools/list,而不是靠定时——才稳定取回结果。

实践提示

如果你要写程序直接和 REA 的 MCP 通道对话,不要用固定 sleep 等待就绪。JSON-RPC 是应答式的,按响应推进即可。另外:若用 Node 脚本收尾,process.exit() 会截断尚未刷盘的异步 stdout——本次实践中两次"结果为空"都是这个原因,改用同步写(fs.writeSync)才拿到完整输出。

数量不对称不等于能力不对称

实测中,CLI 侧用正则从 rea --help 提取到 129 个子命令,MCP 侧 tools/list 返回 170 个工具。两者数量不等,原因在于暴露粒度不同——同一个底层 operation 可能按 surface(CLI / MCP)、按 mode、或按客户端能力差异,投影成不同数量的对外条目。

契约文档对这种粒度有一套明确的展开规则(tool_availability.groups[].names 逐条展开,携带 surface、availability、reason、remediation、default-mode 状态、modes 与 client 需求),并给了一条校验建议:把各组的名字取并集,与 tools/list 比对,即可验证客户端是否收到了完整目录。它还补了一句关键的:某操作若不出现在任何组里,就意味着它从完整清单中缺失——而不是"隐式地不可用"。

09复刻契约:把"逆向一个应用"变成可判定的工程

这是 REA 最独特的一层,也是本次实践真正用到的那部分。它不满足于"给你一堆观测",而是要判定"复刻到什么程度算完成"。

仓库里有一组专门的设施,规模远超"顺手加个功能"的程度:

表 6 · 复刻相关设施(源码路径实测)
组成部分文件作用
义务台账服务ReconstructionObligationLedgerService.ts
+ Authority / Candidates / Policies / Diagnostics / Evaluation / Projection
把认证过的证据转成确定性的复刻主张列表
覆盖度评估ReconstructionCoverageService.ts
+ ReconstructionObligationLedgerEvaluation.ts
判定复刻覆盖到什么程度
配套文档docs/reconstruction-obligation-ledgers.md
docs/reconstruction-readiness.md
给出完整请求示例与契约规定
CI 验证器scripts/verify-reconstruction-readiness.mjs
+ src/contracts/reconstructionReadinessExample.ts
在干净检出上跑 9 阶段一致性验证
领域测试ManagedReconstructionService.test.ts
managedReconstruction.ts
JavaScriptArtifactReconstruction.ts
按目标类型分别实现复刻重建

义务台账:闭合条件

义务台账(obligation ledger)的核心机制可以概括成一句话:把"应该复刻什么"变成带闭合条件的条目,然后逐条判定它闭没闭合。

一条必需的复刻义务只有在同时满足以下全部条件时才能闭合:

  1. 一个 manifest binding 提供了唯一的实现方(unique owner);
  2. 提供了所有必需的 parser / schema / domain 类型;
  3. 提供了每一个必需的用例 fixture;
  4. 有一个通过的验证器,且该验证器的 authority 与原始观测相当;
  5. 该验证器枚举了这条义务的 ID;
  6. 验证结果存在于输入证据包中。

而下列任何一条都会让闭合保持 open 或转为 failed:矛盾、重复的定义或实现方、残留未知、缺失依赖、权威不可用。

义务条目的字段设计本身就是一份方法论:

obligation_id
obligation_version
稳定 ID + 版本号。文档原文:"稳定 ID 与版本让改动可复核"。
application_layer
family
义务落在哪一层(如 cli)、属于哪个族(如 argument-parsing)。
target
四选一定位:artifact_sha256 / application_node_id / semantic_node_id / location。既能锚到字节,也能锚到语义节点。
required_case_kinds
必需用例类型。文档示例里是 ["positive","negative","malformed"]——正常路径、拒绝路径、畸形输入三者都要有。
required_original_authority
required_fixture_authority
required_verifier_authority
三级权威要求。示例值:process / packaged-process / packaged-process。这是"不能拿静态结论证明运行时事实"的落地形态。
residual_unknown_ids
unavailable_authority
残留未知与不可用权威必须显式列在义务上,不能省略。
required_next_evidence
明确的下一步取证动作。示例:"Capture the installed CLI for all three cases."
review_evidence_ids
支持这条义务的评审记录(ev_…)。义务本身也必须有证据支撑。

最关键的一条:空输入不声称闭合

文档里给了一个刻意留空的请求示例,并解释了它的用途:可以拿它来检查客户端集成。它返回一个 unknown 且零义务的账本。文档原文强调:"源证据缺失永不声称闭合"(absence of source Evidence never claims closure)。

这条把"没做"和"做完了"严格分开——一个退回空结果的工具,永远不会被误读成"复刻已验证完成"。同一条价值观还体现在:静态应用图事实只能产生候选义务,不能证明运行时或进程行为。

9 阶段一致性契约

仓库另有一套"复刻就绪度"契约,通过 npm run verify:readiness 在干净检出上执行,并且不对公众暴露为 CLI 命令或 MCP 工具(文档明确说明:它是项目自己的验证器,不是产品能力)。它规定了 9 个必需阶段:

01 发现 02 诊断 03 声明 04 静态 05 场景 06 比较 07 矛盾 08 闭合 09 重放 fail · unknown · unsupported · truncated · skipped —— 永不聚合为 pass 每阶段与每项必需检查必须恰好出现一次,并链接到嵌入证据包中的记录
图 9 · 9 个必需阶段(完整名称见下方表格)。红色虚线标出这套契约的性格:五种非通过状态永不聚合为通过,且每个阶段与每项检查必须恰好出现一次——重复出现或缺失同样不合格。(数据来源:docs/reconstruction-readiness.md 的 Required stages 表)
表 7 · 9 阶段的必需证明(译自 docs/reconstruction-readiness.md)
#阶段必需证明
01discover-classifytarget-free 启动、产物库存、兼容工作流选择,以及精确的可见限制
02diagnose-environment把"损坏的运行时"与"分析失败"分开归因;检测版本偏差;恢复后 revision 已变
03declare-request精确的 target、operation、lifecycle 在启动之前已被请求携带
04static-analysis原生与 JavaScript 双路路由;保留歧义、截断、不支持的语法与残留未知
05reactive-scenarios谓词驱动的交互、后代结算、Electron 关联、取消清理的证据
06compare-authority-candidate显式的偏序 / 有限迹语义;主分歧本地化;比较不完整时给出 unknown
07preserve-contradictions记录并继续;受影响的等价性被阻断;未受影响的比较被保留
08verify-reconstruction-closure检测到不完整复刻;增量验证器闭合;拒绝"权威委托"型候选
09export-replayCLI/MCP 一致;进程与结构化失败一致;确定性重放;篡改与过期输入被拒绝

fail-closed:12 条拒绝就绪的情形

契约还列了一张"什么时候判定为不就绪"的清单。它比"通过条件"更能说明设计取向——因为这里每一条都是主动拒绝给出结论:

最后一条尤其能说明"可复现"是被当作硬要求写进契约的:连"上次的源摘要和这次不一样"都必须被检测出来并拒绝。

这一层对本次实践的意义

我在便笺复刻的规划里自定了一套 ■/□ 标记来表示"证据强 / 证据弱"。读完这套契约才明白差距在哪:我的标记只是我自己的判断,而 REA 的 obligation 是可被机器校验的字段约束——它要求每条义务挂 stable ID、挂 authority 等级、挂必需用例类型、挂支持证据 ID,并且拒绝用低权威证据闭合高权威义务。

如果重做那份规划,正确形态应该是产出一份 obligation ledger(哪怕手工写),而不是一张带标记的规格表。

10进程基础层与工程纪律

编排层要长期跑别人的引擎、启别人的进程、读别人写的文件——这一层的约束条件,比功能更能看出项目的成熟度。

官方架构图里有一个专门的 subgraph 叫 Provider Process Foundation,包含 src/process/(59 个文件)与 src/windows/。它的标注是:owned process groups, private roots, deadlines, bounded file reads and diagnostics, cleanup。这六项就是 REA 对"如何安全地跑一个第三方引擎"给出的答案。

provider 自有进程 不属于调用方,不被共享 owned process groups 自有进程组 private roots 私有根目录 deadlines 时限 bounded file reads 有界文件读取 diagnostics 诊断留存 cleanup 清理
图 10 · 进程基础层对每个 provider 自有进程施加的六项约束。注意"私有根目录"与"有界文件读取"这两项——它们不是性能优化,而是把第三方引擎可能造成的影响限制在可控范围内。配合第 03 章的 verified / unavailable 所有权观测,自有资源的泄漏是可查的。(数据来源:docs/architecture.mermaid)

安装期安全审计

本次实践在安装前对 npm 包做了一轮审计。结论如下——这一节的方法比结论更有参考价值:

表 8 · rea-agents@6.4.0 安装期行为审计(实测)
检查项结果含义
preinstall / install / postinstall均不存在安装时不执行任何代码。这是最要紧的一条——安装期脚本是供应链攻击最常见的入口
prepare / prepack存在但只在 git 检出安装与 publish 时触发,从 registry 装 tarball 不会跑
发布者GitHub Actions OIDC受信发布(非个人 token),带 npm provenance
完整性sha512npm 安装时自动校验
native optionalDependencies无不下载预编译原生二进制
外部副作用需显式同意文档明确:写客户端配置、安装引擎都需另行显式同意

顺带说一个被跳过的安装路径

官方文档提供了一条 curl -fsSL … | bash 的安装方式。本次实践没有采用,而是走 npm 包路径。原因是审计面差异很大:直接执行远程脚本无法在安装前做上述任何检查,而 npm 路径可以逐项验证。能审计的路径优先于方便的路径——这条与 REA 自己的 fail-closed 取向倒是同构的。

生命周期:一个被推翻的担心

本次实践用独立前缀安装(~/.local/share/rea + 软链),而不是 npm 全局。安装后我产生过一个担心:既然 npm root -g 指向的位置与我们的实际安装位置不同,那么 rea setup 注册客户端时会不会写入一个坏掉的启动器路径?

实测证明这个担心不成立:

$ rea setup --client codex --dry-run
  Command: "/Users/junjian/.local/share/rea/lib/node_modules/rea-agents/scripts/rea.mjs" "mcp"
                                    ↑ 正是我们的实际安装位置

$ rea update
  {"status":"current","currentVersion":"6.4.0","installMethod":"npm"}

REA 用的是自身路径自省,而不是去问 npm root -g。所以"装在哪"这个问题,它自己知道答案,不需要外部约定。

顺带记一条实测到的 Node 生态机制,它解释了为什么当初必须显式指定前缀:nvm 的 npm 首行是 #!/usr/bin/env node,所以用绝对路径调用它,它仍然会去跑 PATH 上的那个 node,从而把 prefix 解析成那个 node 的目录。显式 --prefix 因此是必要且正确的。

doctor 的退出码语义

实测中 rea doctor 返回退出码 1。这不是环境坏了——它的检查项里同时包含"本机装了哪些受支持的客户端"和"引擎是否就绪",于是未注册的客户端与未安装的引擎都被计入不健康项。

真正需要关注的是环境侧的判定。契约文档对 doctor 的职责描述是:它检查受支持的 JSON/TOML 客户端注册,把它们的命令向量归类为 aligned / stale / missing / invalid 四态,并且在没有活动连接提供身份之前,把实时服务器状态保持为 unknown。

四态分类 + 一个诚实的 unknown,这又是一次"宁可分类细一点,也不含糊说一句配置有问题"。

工程纪律的几个可观测量

表 9 · 工程实践的可观测特征(实测)
特征观测值说明
仓库规模2,957 个文件不含依赖,纯源码树
提交数2,444—
测试框架vitest + @fast-check/vitest含属性测试(property-based),不只是示例测试
静态检查oxlint, oxfmt, knip, jscpd含死代码检测与重复代码检测
提交守卫husky, lint-stagedpre-commit 阶段拦截
构建rolldown, turbo现代打包器 + 任务编排
契约测试src/contracts/*.test.ts每个工具契约都有对应测试文件

其中 knip(死代码)与 jscpd(重复代码)这两项值得单独注意:它们是"清理型"检查,不是功能型检查。一个项目愿意在 CI 里跑这两样,说明它把结构性整洁当成了持续约束,而不是一次性重构。

11失效模式,与三条被实测推翻的判断

这一章是全文最该读的一章。前面十章的机制再准确,也不如"我错在哪、为什么错"来得有用。

三条更正

原判断 1 npm root -g 与实际安装位置不一致, setup 会注册一个坏掉的启动器路径 实测:不成立 setup 用自身路径自省,正确解析到实际位置 update 也正常:installMethod = npm 原判断 2 没分析 Assets.car 是我的疏漏, 补跑 inspect-asset-catalog 就能补上 实测:是我的误判 REA 自己也打不开:target_unavailable unsupported binary format —— 是工具边界 原判断 3 接入 Ghidra 只需设置环境变量 GHIDRA_INSTALL_DIR 即可 实测:不完整 universal Mach-O 报 architecture_unsupported 须先把二进制 thin 成单架构才能进入分析
图 11 · 三条被实测推翻的判断。左栏是我在交付物里写过的原话,右栏是实测结果。三条都不是"REA 的缺陷"——一条是我的推测未经验证,一条是我把自己的疏漏安错了地方,一条是结论下得太快。它们共同的教训是:机制类结论必须落到一次真实调用上,否则很容易写成"听起来对但跑不通"的话。

更正一:路径隐患不存在

本次改用独立前缀安装后,我担心 rea setup 会基于 npm root -g 拼出错误的启动器路径。--dry-run 实测显示它解析到的是真实安装位置。原因是它用自身路径自省。这条更正的含义是:不要用"通用生态常识"去推测一个具体工具的实现——REA 在这件事上做得比通用做法更可靠。

更正二:Assets.car 不是我的疏漏

我在复刻规划的自我批评里,把"完全跳过了 Assets.car"列为自己的遗漏。实际补跑后 inspect-asset-catalog 返回 target_unavailable / unsupported binary format。REA 自己也读不了这个文件。这条更正的含义是:先验证工具是否具备某能力,再检讨自己是否遗漏——否则会把工具边界误记成自己的失误,进而去做无效的补救。

更正三:接引擎不只是一个环境变量

我把"设 GHIDRA_INSTALL_DIR 即可"写进了规划。实测报错里出现了 ghidra: architecture_unsupported,并附有说明:v1 无法对 universal Mach-O 强制架构,须先 thin。这意味着接入 Ghidra 的前提条件里,包含一步针对目标文件的前置处理。这条更正的含义是:"装上就能用"通常在编排层工具上不成立——编排层的接入成本不只在你装了什么,还在目标本身的形态。

失效模式清单

把本次实践与契约文档合起来看,REA 会"用不了"或"给不出结论"的情形可以归纳成五类。它们都不表现为崩溃,而是表现为带原因的显式拒绝:

表 10 · 五类失效模式
失效类型典型表现根因处置
引擎缺席 provider_unavailable + not_configured / executable_missing 没装或没配引擎 按 remediation 安装;或改用 target-free 操作
目标不兼容 provider_unavailable + architecture_unsupported,或 target_unavailable + unsupported binary format 文件格式 / 架构超出该 provider 或该操作的处理范围 预处理目标(如 thin),或换操作;不要期待换引擎能解决格式问题
真机观测缺失 静态结论无法支撑运行时主张;义务保持 open 静态 authority 低于原始观测 authority 跑运行时 provider 补证据,而不是放宽闭合条件
客户端容量不足 某些客户端对所有请求失败(单工具 schema 超上限) 函数调用客户端对 schema 字面大小有上限 开 REA_MCP_INPUT_SCHEMA_PROFILE=compact;需重启生效
事实冲突 输出里出现 contradictions,或义务闭合被判 failed 多个证据源互斥 按契约要求记录并继续,阻断受影响的等价性判定,保留未受影响的比较

一处我判不准的边界,如实说明

170 个 MCP 工具里,究竟多少个真正需要引擎?我尝试过两种判法:

按工具名匹配(含 native / _ida / hopper / ghidra / decompile)会漏掉 goto_address、list_documents、procedure_pseudo_code 这类明显依赖活动反汇编会话的名字。

按描述文本匹配又会高估——demangle_swift 的描述里明确写着 "without requiring Hopper",却会被关键词命中。所以 这个边界无法纯静态确定。

我唯一敢引用的可靠数字是:按描述直接点名引擎的工具至少 33 个(Hopper GUI 光标类、Ghidra headless 类、JEB 类等)。至于"引擎绑定工具占多大比例",本报告不给结论——REA 自己给出的机器可读口径是那 18 项 target-free 操作,请以它为准。

本次实践未完成的项

为避免读者把"没提到"误当成"不需要",这里列出我知道但没做的:

12正确用法清单

如果只带走一页,带这一页。每条都对应前面某一章的机制。

判断可用性

  • 用 rea capabilities --json 判断"零引擎下能做什么",不要靠工具名猜
  • 用 binary_session.tool_availability 判断"当前这次调用能不能成",因为 tools/list 是全量目录、不反映当前可用性
  • 分清两种失败:provider_unavailable(缺东西,可补救)vs target_unavailable(这个输入读不了,只能换方法)
  • 把 unknown 当独立状态对待——它既不是可用也不是不可用

组织调查

  • 每个结论挂 evidence_id;没有 ID 的结论是"我看到的",不是"可复核的"
  • 区分观测与推断——REA 自己就是这么分桶的(observations vs hypotheses)
  • 把"没查清"写进输出,而不是留给读者猜。unexplored_branches 和 residual_unknown 都是正式字段
  • 复刻类任务用义务台账的形态组织:稳定 ID + 版本 + 必需用例 + authority 要求

控制成本

  • 大输出用 --token-limit 截断(本次单次 inspect-artifact 输出 426 KB)
  • 直接写程序对话 MCP 时,用事件驱动而非固定等待;冷启动要加载 170 个工具的 schema
  • Node 脚本收尾别用 process.exit(),异步 stdout 会被截断——用同步写
  • 单工具 schema 超客户端上限时开 compact profile(需重启服务器生效)

避免踩坑

  • 不要假定"装了引擎就能分析一切"——架构与格式是可独立否决的条件
  • 不要因为工具清单里有某个工具,就认为它在当前环境可用
  • 不要用低权威证据去支撑高权威结论(静态图 ≠ 运行时行为)
  • 不要指望失败后自动换条路——REA 明确不这么做

一句话总结用法

把 REA 当成一个需要先问清楚"现在能做什么"、再接受"做不到就明说"的执行器。它的用法不舒服的地方,恰好也是它可信的地方——你在它那里拿到的每一个结论,都带着"凭什么"的凭据。

13结论

以下判断是观点,不是共识。它们建立在前十二章的证据上,但推论部分由我负责。

它解决的问题不是"分析能力",是"分析的可复核性"

如果把 REA 的价值理解成"让 AI 能反编译",那它的天花板就是它所编排的引擎——你装了 Ghidra,它就有 Ghidra 的水平。这不是一个有护城河的位置。

但如果把它的价值理解成"让分析结论变得可复核、可比较、可复现",那它做的事情就没有替代品:五元口径把观测与推断分开、authority 分级阻止证据越级、稳定 ID 让结论可追溯、fail-closed 让"没做成"无法伪装成"做成了"。这套东西在引擎换成什么之后都依然成立。

本次实践最有说服力的一个细节:126 KB 的证据包里,contradictions 与 unexplored_branches 是正式字段。一个把"我有哪些没查清"写进输出结构的工具,和一个只输出结论的工具,可信度不在一个层级上。

它的定位代价是真实的

编排层必然要付的代价是:能力上限受制于被编排者。零引擎环境下,REA 能做的是 18 项静态产物分析;要触及反汇编与伪代码,必须自带引擎。doctor 退出码 1、170 个工具里大量不可用、provider_unavailable——这些都是同一个代价的不同表现。

但这个代价换来的是可裁剪性:不装引擎不会让任何一项已有能力失效(第 05 章的两栏没有交集)。对使用者来说,这意味着一件事:你可以从零成本开始用,然后按需加装,而不是必须先配齐一整条工具链。

对"AI 原生工具"的一点观察

REA 是少数把失败路径设计得和成功路径一样完整的工具。它有三套显式的失败分级(provider / target / authority)、一个诚实的三态可用性(含 unknown)、一份 12 条的就绪拒绝清单,以及"缺少源证据永不声称闭合"的明文规定。

这与"AI Agent 需要什么"这个问题直接相关:Agent 最容易犯的错不是"不会做",而是"不知道自己做没做成"。一个把"我做到了什么程度、还差什么、下一步该采什么证据"结构化输出的工具,比一个"能力强但只说结论"的工具更适合被编排进 Agent 流程——因为后者会让 Agent 在不确定的地方自信地继续往前走。

最后一个判断

本次实践的起点是"用 REA 复刻一个 macOS 应用"。走到最后,REA 给的最大收获不是它解出了多少 nib 对象(780 个节点确实有用,但那只占整件事的一小部分),而是它示范了一种把"逆向"从手工技艺变成工程记录的做法:先声明要证明什么,再为每条主张配置证据要求与权威等级,最后用机器判定闭合。

这套做法与工具无关。就算不用 REA,产出物也应该长成那个样子。

14附录:本次实践的命令与产物

实跑过的 REA 命令

# 版本与能力面
rea --version                                    → 6.4.0
rea --help                                       → 129 个子命令
rea capabilities --json                          → 18 项 target-free,全 available
rea doctor --json                                → node / host 双绿
rea capabilities / providers                     → 目录身份摘要

# 目标分析(零引擎路径)
rea inspect /System/Applications/Stickies.app    → provider_unavailable(引擎绑定)
rea decode-interface-builder <app> --format json → 780 节点 / 421 边 / 3 nib
rea inspect-artifact <app> --format json         → 426 KB,ev_68273f72…
rea inspect-asset-catalog <Assets.car>            → target_unavailable
rea analyze-javascript-application <fixture>     → 退出码 0,11 阶段全走完

# 生命周期
rea setup --client codex --dry-run               → 解析到真实安装路径
rea update --json                                → status: current

# MCP 通道
rea mcp  (stdio JSON-RPC)
  → initialize (2024-11-05) ─→ serverInfo rea/6.4.0
  → tools/list ─→ 170 个工具,170/170 带 inputSchema

本次实践产出的三份交付物

产物规模与本文的关系
rea-deep-research.html13 章报告对 REA 项目本身的深度研究报告(热度、工程质量、法律边界、竞争格局)
stickies-replica-plan.md复刻规划基于 REA 实测与只读侦察做的复刻方案;本文第 11 章的更正针对它
noteboard.html1,448 行最终落地的单文件 HTML 便签应用;规格来自 nib 解码的 outlet 与 selector

关键数据来源

本文未使用任何 Apple 的界面文案、图标或资源文件。所有引用 REA 仓库内容的部分均已注明文件路径,读者可自行复核。