00摘要:十二条核心论断
每条都是可证伪的机制判断,后面各章逐一展开。证据标记见表末说明。
其中反编译引擎 0 个
(target-free,全部可用)
(CLI 侧 129 个子命令)
421 条边
- REA 自己不产出分析知识。它把"知识"外包给 provider,自身只负责调度、约束与存档。证据:
package.json的 36 个运行时依赖里,没有任何原生反编译引擎;唯一带分析语义的依赖是evmole(EVM 字节码,纯 WASM 可内联)。 源码 - 它的调度单位是 provider,不是工具。170 个 MCP 工具只是 provider 能力的投影面;引擎(Ghidra/Hopper/IDA/JEB)本身也只是 provider 的一种实现。证据:官方架构图把 13 个 provider 与
src/server/(仅做请求转译)分列为不同层。 源码 - provider 选择是确定性的,且失败即报错不猜测。注册表按排序取候选、命中即用、无模糊回退。实测对
Stickies.app调rea inspect,直接返回provider_unavailable并逐条列出四个引擎各自的不可用原因。 实测 - 能力面天然分两层,且可以在零引擎下完全运行。
rea capabilities --json返回 18 项 target-free 操作,本机零引擎环境下 18/18 全部 available。这条"纯净子集"是它可裁剪性的来源。 实测 - "操作可用"不等于"对任意目标可用"——这是两套不同的可用性。
inspect_asset_catalog在能力目录里是available: true,但真拿Assets.car去调,返回target_unavailable / unsupported binary format。能力声明管"操作存不存在",目标准入管"这个输入能不能读"。 实测 - 它用一张"产物图"统一所有目标类型。nib、JavaScript 应用、.NET 程序集、Mach-O、APK 都归一到
nodes + edges + kind的同一结构。实测decode-interface-builder对 3 个 nib 产出 780 节点 / 421 边。 实测 源码 - 它真正可移植的资产是证据模型,不是分析算法。输出结构固定为
observations / hypotheses / contradictions / unexplored_branches / next_probes,每份证据带稳定 ID 与authority分级。换掉引擎,这套口径不变。 实测 - 复刻不是附带功能,是一等公民。仓库里有独立的义务台账服务、覆盖度服务、9 阶段一致性契约,以及
docs/reconstruction-obligation-ledgers.md与docs/reconstruction-readiness.md,并有 CI 脚本verify-reconstruction-readiness.mjs。 源码 - 它的判定是 fail-closed:不合格一律不聚合为通过。契约明确规定
fail、unknown、unsupported、truncated、skipped永不聚合为 pass;每阶段与每项检查必须恰好出现一次并链接到证据。 源码 - CLI 与 MCP 是同一内核的两个薄适配器,能力等价。官方架构图里
scripts/rea.mjs只是 dispatcher,把请求路由到src/cli.ts(one-shot)或src/main.ts(MCP stdio),两者共用src/application/与src/composition/。所以未激活 MCP 不构成能力损失。 源码 实测 - 它的价值上限由你装的 provider 决定。这是"编排层"定位的必然代价——工具再多,反汇编深度仍取决于你装了 Ghidra 还是 Hopper。反过来也成立:不装引擎时它退化成一套诚实、可复现的静态产物分析器,而非什么都做不了的空壳。 推断
- 它把"分析失败"与"环境损坏"分开归因。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 个运行时依赖,零引擎
这是最直接的判据。一个真正的反编译器必须依赖反编译算法;如果依赖树里一个都没有,它在结构上就不可能是反编译器。
evmole——EVM 字节码反编译器,且是纯 WebAssembly 实现,可内联进进程而不需要外部可执行文件。这个细节恰好体现了架构偏好:能内联的就内联,需要外部引擎的一律交给用户自备。(数据来源:package.json dependencies 字段,36 项全列)| 职责类别 | 数量 | 代表包 | 与"分析能力"的关系 |
|---|---|---|---|
| 结构化文本解析 | 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-mapping | CLI 交互、确定性序列化、结果比对、原子落盘、源码映射 |
| 终端与进程 | 6 | @lydell/node-pty, @xterm/headless, @xterm/addon-serialize, cross-spawn, stream-chain, stream-json | 为"自有子进程"提供终端仿真与结构化流——是运行被测程序的基础设施 |
| Schema 与校验 | 4 | ajv, ajv-formats, zod, semver | 输入契约与版本约束的运行时校验,是"确定性"的前提 |
| 配置与清单 | 3 | yaml, plist, har-schema | 读客户端配置、Apple plist、HTTP 归档 |
| 归档与打包 | 2 | @zip.js/zip.js, @electron/asar | 解包 zip / ASAR,属产物层 |
| 浏览器与网络 | 2 | playwright-core, ws | CDP/Playwright 驱动真实浏览器做动态观测 |
| MCP 协议 | 2 | @modelcontextprotocol/client, @modelcontextprotocol/server | 对外接口层的 SDK |
| 日志与诊断 | 1 | pino | 结构化日志 |
| 版本控制 | 1 | isomorphic-git | 纯 JS 的 git 客户端——用于分发 bundled skill |
| 字节码分析 | 1 | evmole | 全树唯一的分析型引擎,且限定在 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)只被装配层引用。
src/config/ 为横切关注点、测试 tests/、评估 src/evaluation/)。层号自上而下递增,依赖只向内流动:入口层依赖应用层,应用层依赖契约与领域层,领域层不依赖任何其它层。provider 层不在任何入口的直接依赖路径上——它只被组合层装配。这正是"换引擎不牵动其它部分"的结构保证。(数据来源:docs/architecture.mermaid)三件关键装置
官方架构图在应用层单独点名了三个装置。它们是理解 REA 运行时行为的钥匙:
+ BinarySession
provider_unavailable 里那份"逐条列出为什么用不了"的候选清单的来源。+ Unknown ownership
EvidenceLedger.residualUnknown.test.ts——"残留未知"是被单独测试的一等概念,而不是遗漏。这与第 07 章的失败哲学直接相关。Provider 层的真实规模
官方图列了 13 个 provider subgraph。按文件数看它们的权重差异很大——这个分布本身就是一种表态:
| Provider | 文件数 | 性质 | 引擎依赖 |
|---|---|---|---|
| src/browser/ | 113 | CDP / Playwright 动态观测 | 内联 需真实浏览器 |
| src/ghidra/ | 60 | 反汇编 / 伪代码 / 引用 / CFG | 外部 Ghidra 自备 |
| src/process/ | 59 | 自有进程组、私有根、时限 | 无 基础层 |
| src/artifacts/ | 58 | 产物库存与抽取、格式读取 | 自研 无需外部 |
| src/native/ | 55 | pwntools/pwndbg BYO 适配 | 外部 上游未改动 |
| src/dotnet/ | 34 | .NET 静态元数据(PE/CLI) | 自研 静态解析 |
| src/hopper/ | 29 | Hopper 启动 + Unix socket 协议 | 外部 Hopper 自备 |
| src/ida/ | 23 | 上游 ida-pro-mcp 契约适配 | 外部 上游生态 |
| src/javascript/ | 23 | sourceMap 编解码、堆限制进程 | 自研 固定上游 codec |
| src/firmware/ | 10 | Binwalk / Unblob 适配 | 外部 自带进程 |
| src/evm/ | 10 | EVMole/WASM 受限 worker | 内联 WASM |
| src/android/ | 8 | 静态 APK 适配、pin 住 JADX | 外部 再分发 |
| src/inspector/ | 8 | 被动 V8 Inspector 发现与捕获 | 内联 运行时 |
| src/reference/ | 8 | 参考源文件读取 | 无 纯 IO |
| src/jeb/ | 5 | JEB 适配 | 外部 商业许可 |
一个值得注意的分布
四个"外部引擎"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 报错结构。
analysis_run.run_id。红色部分标出失败路径的关键性质——它不尝试替代方案,而是把失败类型化后如实返回。(依据:docs/mcp-contracts.md、docs/architecture.mermaid、实测 provider_unavailable 输出)逐段说明
| 段 | 名称 | 发生什么 | 依据 |
|---|---|---|---|
| 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/ |
| 04 | Provider 选择 | 先做 target-free 发现(按 provider ID 排序、报告 host 可用性与 unknown 目标支持情况、不创建 analysis client),再按确定性策略选取。选中即绑定,见第 04 章。 | mcp-contracts.md |
| 05 | run 分配 | 分配 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 的几个字段做了明确的区分,这几条合起来就是"确定性"的完整定义:
(顶层)
auto;使用与 CLI --provider 和 REA_ANALYSIS_PROVIDER 同一套解析与选择策略。这是 CLI/MCP 能力等价的一个具体证据。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 甚至没进入候选)
三点值得注意:
- 按"操作"失败,不按"工具"失败。报的是
operation = current_document。这与架构图里"operation"是路由的一等公民一致——工具只是 operation 的对外投影。 - 同一候选可以有多个卡点。
ghidra出现了两次:既未配置,又架构不支持。architecture_unsupported这一条特别有信息量——它说明这类检查带有目标侧约束,不只是"环境里有没有"。 - 没有进入候选的引擎不会出现。
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 的操作。这份清单是本次实践最有用的产出之一——它精确划出了"不装任何东西也能做什么"的边界。
rea capabilities --json 与 rea inspect 实测输出)18 项零引擎能力全清单
按作用对象分组,这 18 项构成了一套完整的"静态产物分析"工具集:
| 分组 | 操作 | 项 | 作用 |
|---|---|---|---|
| Apple / 产物 | decode_interface_builder | 11 | 解析 nib/xib 成产物图(本次实践的主力) |
| inspect_keyed_archive | 解析 NSKeyedArchiver 归档 | ||
| inspect_asset_catalog | 读取 Assets.car 元数据 | ||
| inspect_macho | Mach-O 头部与加载命令 | ||
| inspect_plist | 属性列表解析 | ||
| inspect_signature | 代码签名与授权信息 | ||
| list_architectures | 列出 fat binary 中的架构切片 | ||
| demangle_swift | Swift 符号反混淆(不需要 Hopper) | ||
| trace_dylib_resolution | 逐条追踪动态库解析路径 | ||
| extract_artifact | 从包中抽取子产物 | ||
| inspect_artifact | 产物库存 + 后续探测建议 | ||
| 托管代码 | inspect_managed_artifact | 3 | .NET / CLI 程序集元数据 |
| inspect_managed_members | 类型与成员清单 | ||
| inspect_managed_native_boundaries | 托管到原生的边界 | ||
| 运行观测 | capture_native_ui_scenario | 4 | 观测原生 UI 交互场景(本次未跑,见第 11 章) |
| observe_native_ui | 原生窗口/控件观测 | ||
| observe_native_calls | 原生调用序列观测 | ||
| inspect_native_dispatch_metadata | Objective-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 专用输出",而是通用产物图。
view / control / outlet)表达结构,下半三类(objc_selector / action / constraint)表达行为与关系。本次实践中,正是这后三类让功能面得以还原——从图里直接读出了 80 个方法选择器与 11 个 outlet,而不是靠界面文案反推。(数据来源:rea decode-interface-builder JSON 输出的 normalized_result.graph)节点与边的字段
从实测输出的 JSON 结构可以直接读出图的模型:
…/SNDocument.nib/…)。因此一个节点天然知道它来自哪个文件。view、control、resource、constraint、objc_selector、action、outlet。interface_builder_object_id
NSSelectorName、NSMenuItems)。这是行为信息的来源——方法名就藏在这里。同构的姊妹实现
产物图不是只有 Apple 一种。仓库里能数出一组平行实现,共用同一套"节点 + 边"抽象:
| 领域实现 | 作用对象 | 同构点 |
|---|---|---|
| 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 对"一次调查"的定义:
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 }
三个细节:
- ID 带类型前缀。
ev_是证据、ai_是 inspection。前缀让 ID 在日志和引用中自带语义,不需要额外查表。 - inspection_id 是 64 位十六进制——长度暗示它是内容摘要而非自增序号。配合目录摘要
catalog_digest,一次调查可以被完整锚定。 next_probes带authority。它不只是说"建议下一步做什么",还说"这一步的证据权威性属于哪一级"。
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)。所以适配层是薄的,内核是共享的。
rea --help 实测、MCP 工具清单实测、docs/mcp-contracts.md)能力等价的三个结构证据
src/composition/——provider 与 session 工厂是共享的,不存在"某条路径少一个 provider"的可能。open_binary.provider_id、CLI --provider、环境变量 REA_ANALYSIS_PROVIDER 使用同一套解析与选择策略(契约原文:it uses the same parser and selection policy as 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 最独特的一层,也是本次实践真正用到的那部分。它不满足于"给你一堆观测",而是要判定"复刻到什么程度算完成"。
仓库里有一组专门的设施,规模远超"顺手加个功能"的程度:
| 组成部分 | 文件 | 作用 |
|---|---|---|
| 义务台账服务 | 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)的核心机制可以概括成一句话:把"应该复刻什么"变成带闭合条件的条目,然后逐条判定它闭没闭合。
一条必需的复刻义务只有在同时满足以下全部条件时才能闭合:
- 一个 manifest binding 提供了唯一的实现方(unique owner);
- 提供了所有必需的 parser / schema / domain 类型;
- 提供了每一个必需的用例 fixture;
- 有一个通过的验证器,且该验证器的
authority与原始观测相当; - 该验证器枚举了这条义务的 ID;
- 验证结果存在于输入证据包中。
而下列任何一条都会让闭合保持 open 或转为 failed:矛盾、重复的定义或实现方、残留未知、缺失依赖、权威不可用。
义务条目的字段设计本身就是一份方法论:
obligation_version
family
cli)、属于哪个族(如 argument-parsing)。artifact_sha256 / application_node_id / semantic_node_id / location。既能锚到字节,也能锚到语义节点。["positive","negative","malformed"]——正常路径、拒绝路径、畸形输入三者都要有。required_fixture_authority
required_verifier_authority
process / packaged-process / packaged-process。这是"不能拿静态结论证明运行时事实"的落地形态。unavailable_authority
ev_…)。义务本身也必须有证据支撑。最关键的一条:空输入不声称闭合
文档里给了一个刻意留空的请求示例,并解释了它的用途:可以拿它来检查客户端集成。它返回一个 unknown 且零义务的账本。文档原文强调:"源证据缺失永不声称闭合"(absence of source Evidence never claims closure)。
这条把"没做"和"做完了"严格分开——一个退回空结果的工具,永远不会被误读成"复刻已验证完成"。同一条价值观还体现在:静态应用图事实只能产生候选义务,不能证明运行时或进程行为。
9 阶段一致性契约
仓库另有一套"复刻就绪度"契约,通过 npm run verify:readiness 在干净检出上执行,并且不对公众暴露为 CLI 命令或 MCP 工具(文档明确说明:它是项目自己的验证器,不是产品能力)。它规定了 9 个必需阶段:
docs/reconstruction-readiness.md 的 Required stages 表)| # | 阶段 | 必需证明 |
|---|---|---|
| 01 | discover-classify | target-free 启动、产物库存、兼容工作流选择,以及精确的可见限制 |
| 02 | diagnose-environment | 把"损坏的运行时"与"分析失败"分开归因;检测版本偏差;恢复后 revision 已变 |
| 03 | declare-request | 精确的 target、operation、lifecycle 在启动之前已被请求携带 |
| 04 | static-analysis | 原生与 JavaScript 双路路由;保留歧义、截断、不支持的语法与残留未知 |
| 05 | reactive-scenarios | 谓词驱动的交互、后代结算、Electron 关联、取消清理的证据 |
| 06 | compare-authority-candidate | 显式的偏序 / 有限迹语义;主分歧本地化;比较不完整时给出 unknown |
| 07 | preserve-contradictions | 记录并继续;受影响的等价性被阻断;未受影响的比较被保留 |
| 08 | verify-reconstruction-closure | 检测到不完整复刻;增量验证器闭合;拒绝"权威委托"型候选 |
| 09 | export-replay | CLI/MCP 一致;进程与结构化失败一致;确定性重放;篡改与过期输入被拒绝 |
fail-closed:12 条拒绝就绪的情形
契约还列了一张"什么时候判定为不就绪"的清单。它比"通过条件"更能说明设计取向——因为这里每一条都是主动拒绝给出结论:
- 一个有界能力没有给出有效的数值限制;
- 一个 fixture 缺少恰好一个兼容的选中工作流;
- 调用了不兼容或不可用的 provider;
- 类型化的 CLI 失败却以 0 退出,或 MCP 失败却报告成功;
- 并发行为使用了全域序比较;
- 把截断、不稳定、权威不可用或矛盾标记为等价;
- 一处刻意的主分歧没有被本地化;
- 矛盾证据或受影响的比较缺失;
- 复刻义务台账不是
ready、没有必需义务,或保留了未归属/未验证的必需义务; - 进程超出其声明的 target / lifecycle、取消丢失诊断信息,或自有资源未释放;
- 重放不确定、篡改与过期未被检测,或预期源摘要不符。
最后一条尤其能说明"可复现"是被当作硬要求写进契约的:连"上次的源摘要和这次不一样"都必须被检测出来并拒绝。
这一层对本次实践的意义
我在便笺复刻的规划里自定了一套 ■/□ 标记来表示"证据强 / 证据弱"。读完这套契约才明白差距在哪:我的标记只是我自己的判断,而 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 对"如何安全地跑一个第三方引擎"给出的答案。
verified / unavailable 所有权观测,自有资源的泄漏是可查的。(数据来源:docs/architecture.mermaid)安装期安全审计
本次实践在安装前对 npm 包做了一轮审计。结论如下——这一节的方法比结论更有参考价值:
| 检查项 | 结果 | 含义 |
|---|---|---|
preinstall / install / postinstall | 均不存在 | 安装时不执行任何代码。这是最要紧的一条——安装期脚本是供应链攻击最常见的入口 |
prepare / prepack | 存在 | 但只在 git 检出安装与 publish 时触发,从 registry 装 tarball 不会跑 |
| 发布者 | GitHub Actions OIDC | 受信发布(非个人 token),带 npm provenance |
| 完整性 | sha512 | npm 安装时自动校验 |
| 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,这又是一次"宁可分类细一点,也不含糊说一句配置有问题"。
工程纪律的几个可观测量
| 特征 | 观测值 | 说明 |
|---|---|---|
| 仓库规模 | 2,957 个文件 | 不含依赖,纯源码树 |
| 提交数 | 2,444 | — |
| 测试框架 | vitest + @fast-check/vitest | 含属性测试(property-based),不只是示例测试 |
| 静态检查 | oxlint, oxfmt, knip, jscpd | 含死代码检测与重复代码检测 |
| 提交守卫 | husky, lint-staged | pre-commit 阶段拦截 |
| 构建 | rolldown, turbo | 现代打包器 + 任务编排 |
| 契约测试 | src/contracts/*.test.ts | 每个工具契约都有对应测试文件 |
其中 knip(死代码)与 jscpd(重复代码)这两项值得单独注意:它们是"清理型"检查,不是功能型检查。一个项目愿意在 CI 里跑这两样,说明它把结构性整洁当成了持续约束,而不是一次性重构。
11失效模式,与三条被实测推翻的判断
这一章是全文最该读的一章。前面十章的机制再准确,也不如"我错在哪、为什么错"来得有用。
三条更正
更正一:路径隐患不存在
本次改用独立前缀安装后,我担心 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 会"用不了"或"给不出结论"的情形可以归纳成五类。它们都不表现为崩溃,而是表现为带原因的显式拒绝:
| 失效类型 | 典型表现 | 根因 | 处置 |
|---|---|---|---|
| 引擎缺席 | 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 操作,请以它为准。
本次实践未完成的项
为避免读者把"没提到"误当成"不需要",这里列出我知道但没做的:
capture-native-ui-scenario/observe-native-ui—— 这 4 项运行时观测能力可用但未跑。它们是补上 nib 几何缺失(第 06 章那个边界)的唯一途径,也是复刻规划里 P0 行为观测阶段的核心。本机工具链(Xcode CLI)已具备,未跑的原因是需要在目标机上真实启动应用。project-apple-application-graph—— 未跑。它能把已采集的库存证据投影成应用图,是复刻规划的下一步。build-reconstruction-obligation-ledger/evaluate-reconstruction-coverage—— 未跑。第 09 章讲的机制来自文档与源码,不是执行结果;我给的字段清单有文档依据,但没有实际产出过一份 ledger。- DSh 客户端注册 —— npm 6.4.0 不含
dsh适配器,需要从源码构建,未做。 - 真实引擎端到端 —— 未装任何引擎,所以"装上引擎后 REA 表现如何"这一半,本文无法评价。
12正确用法清单
如果只带走一页,带这一页。每条都对应前面某一章的机制。
判断可用性
- 用
rea capabilities --json判断"零引擎下能做什么",不要靠工具名猜 - 用
binary_session.tool_availability判断"当前这次调用能不能成",因为tools/list是全量目录、不反映当前可用性 - 分清两种失败:
provider_unavailable(缺东西,可补救)vstarget_unavailable(这个输入读不了,只能换方法) - 把
unknown当独立状态对待——它既不是可用也不是不可用
组织调查
- 每个结论挂
evidence_id;没有 ID 的结论是"我看到的",不是"可复核的" - 区分观测与推断——REA 自己就是这么分桶的(
observationsvshypotheses) - 把"没查清"写进输出,而不是留给读者猜。
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.html | 13 章报告 | 对 REA 项目本身的深度研究报告(热度、工程质量、法律边界、竞争格局) |
| stickies-replica-plan.md | 复刻规划 | 基于 REA 实测与只读侦察做的复刻方案;本文第 11 章的更正针对它 |
| noteboard.html | 1,448 行 | 最终落地的单文件 HTML 便签应用;规格来自 nib 解码的 outlet 与 selector |
关键数据来源
- 本机 CLI 实测:
rea 子命令的输出,2026-10-12 - 仓库文件:通过 GitHub API 读取的
docs/architecture.mermaid、docs/mcp-contracts.md、docs/reconstruction-obligation-ledgers.md、docs/reconstruction-readiness.md、package.json、以及src/目录树 - npm registry:
rea-agents的版本、依赖、发布者与完整性信息 - MCP 探针:自写的 stdio JSON-RPC 探针(事件驱动)
本文未使用任何 Apple 的界面文案、图标或资源文件。所有引用 REA 仓库内容的部分均已注明文件路径,读者可自行复核。