dsh-plugin-rea 2.0.2 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +92 -0
- package/README.md +16 -7
- package/README.zh-CN.md +15 -6
- package/assets/make-screenshots.ps1 +2 -2
- package/assets/screenshot-1.png +0 -0
- package/bin/rea-shim.mjs +112 -19
- package/check-updates.ps1 +90 -15
- package/docs/bundle-design.md +6 -5
- package/manifest/bundle.manifest.json +48 -41
- package/package.json +2 -3
- package/scripts/audit-manifest.mjs +98 -0
- package/scripts/rea-bundle-bootstrap.ps1 +37 -10
- package/scripts/setup.mjs +352 -87
- package/scripts/setup.test.mjs +683 -4
- package/examples/cordis.patch.posix.yml +0 -18
- package/examples/cordis.patch.with-env.yml +0 -42
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,98 @@
|
|
|
2
2
|
|
|
3
3
|
本插件遵循语义化版本。版本号变化说明「配置组合」这层的变化,与 REA 自身的版本无关。
|
|
4
4
|
|
|
5
|
+
## 2.0.3 — 2026-10-16
|
|
6
|
+
|
|
7
|
+
**修复 11 处已核实的缺陷(每条都先确认存在、并配了"回退即变红"的回归测试);上游 REA 钉子从 6.4.0 升到 6.5.0。**
|
|
8
|
+
|
|
9
|
+
### 上游
|
|
10
|
+
|
|
11
|
+
- `pins.reaAgents` 与 `components.reaAgents.sources.npm.version` → **6.5.0**:engines 与 bin 入口未变
|
|
12
|
+
(`^22.19.0 || ^24.11.0 || >=26.0.0`、`scripts/rea.mjs`),工具数 170 → **207**(运行时 `rea doctor --json` 实测;上游源码静态清点为 206,多出的 1 个注册在 contracts 之外。新增:Frida 9、
|
|
13
|
+
GDB 4、Rizin/Cutter 6、pcap/packet 4、flutter 2、sqlite/rust/go/wasm/electron/harmony/hopper 各 1 等)。
|
|
14
|
+
新增的 `optionalDependencies.frida` 在 `--ignore-scripts` 下不会执行安装脚本。REA 探针正则收紧为
|
|
15
|
+
`^6\.5\.0(\s|$)`,审计新增"下一个 patch/minor 不得通过"的断言。
|
|
16
|
+
- Ghidra 12.1.4 / apktool 3.0.3 / jadx-headless-mcp 0.7.1 **经核实仍是当前最新**;Node 24.22.0 虽已发布,
|
|
17
|
+
但晚于本插件的实测矩阵,故保持 24.21.0(engines 完全满足)。
|
|
18
|
+
|
|
19
|
+
### 修复
|
|
20
|
+
|
|
21
|
+
- **`--only` 抹掉其它组件的记录**:bundle.json 由本轮结果整份重写,未点名组件的 `paths.jdk/ghidra` 变
|
|
22
|
+
`null`,shim 不再导出 `JAVA_HOME`/`GHIDRA_INSTALL_DIR`——原生分析静默失效,下次完整跑还得重新解压
|
|
23
|
+
750 MB。未点名的组件现在按旧记录结转(且只在记录的路径仍然存在时),缺依赖时优先用旧记录/机器/运行
|
|
24
|
+
setup 的解释器来满足,而不是整段跳过检查。
|
|
25
|
+
- **完整的 `.part` 不被当作已完成**:只打一行日志,然后发 `Range: bytes=<全长>-`(416;四次重试后把
|
|
26
|
+
已经下好的文件报成失败)。现在校验通过即改名收下。
|
|
27
|
+
- **Windows 下载主路线没有续传**:PowerShell 路线每轮先删 `.part`;失败后 Node 兜底仍用进本轮前的
|
|
28
|
+
`have`,以 append 把 200 全量接在半成品后面(长度必然超标,只能重下)。两条路线现在都按盘上实际
|
|
29
|
+
长度发 Range 续传。
|
|
30
|
+
- **POSIX 的 apktool wrapper 没有执行位**(copyFileSync 落 0644,而 REA 不加 shell 直接 spawn → EACCES):
|
|
31
|
+
manifest 里标了 `executable` 的条目落盘时 chmod 0755。
|
|
32
|
+
- **apktool wrapper 钉在 `master` 分支 + 硬校验 sha256**:上游对 wrapper 的任何一次提交都会让全网新装
|
|
33
|
+
hash mismatch。改钉不可变的 `v3.0.3` tag,哈希按该 tag 重新实测。
|
|
34
|
+
- **`--reuse` 不校验版本**:JDK 只看 `javac` 存在、Ghidra 只看 `analyzeHeadless` 存在(JDK 8/11、Ghidra 11
|
|
35
|
+
照收)。现在候选必须通过该组件自己的 probe;同时修好"文件型 probe 从不检查 `expect`"(Ghidra 的
|
|
36
|
+
`application.version=` 断言此前是死配置)。
|
|
37
|
+
- **`bundle.json` 损坏即永久失败**:setup 读不了它、也没人启动 setup,用户只能手删文件。现在 setup 容忍
|
|
38
|
+
它(并按需重写),shim 对 `malformed` 与 `missing` 一样触发供给。
|
|
39
|
+
- **失败后 shim 谎报 6 小时**:lock 现在记录状态与 pid;失败态如实报告"上一次失败了",持有者已消失的
|
|
40
|
+
lock 不再阻塞重试。
|
|
41
|
+
- **成功的 `--json` 轮次从不删 lock**:删除语句原先在非 `--json` 分支里,--json 成功也留下 lock。
|
|
42
|
+
- **shim 兜底 REA 路径少了 `node_modules`**:旧 bundle.json 缺 `paths.reaEntry` 时会把可用安装判成"CLI 丢了"。
|
|
43
|
+
- **`scripts/rea-bundle-bootstrap.ps1`**:`node -v` 输出非数字时 `[int]` 强转在 `$ErrorActionPreference='Stop'`
|
|
44
|
+
下会炸掉整个 bootstrap(而它存在的意义就是这条路径);`&` 调用失败时 `$LASTEXITCODE` 可能为 `$null` →
|
|
45
|
+
`exit $null` = 假成功;engines 改从 manifest 的 `pins.nodeEngines` 读(不再抄第二份);`-DryRun` 不再创建
|
|
46
|
+
目录(它承诺"什么都没改")。
|
|
47
|
+
- **`check-updates.ps1` 的 1.x 残留**:REA 节默认找 npm 全局包(2.x 是 vendored)、Ghidra 节读用户级
|
|
48
|
+
`GHIDRA_INSTALL_DIR`、`rea doctor` 拿不到 vendored CLI(整段自检被静默跳过)、一体包对账对"未记录版本"
|
|
49
|
+
的组件静默 `continue` 却打印"全部一致"。现在一律先读 `bundle.json`,并如实报告无法对账的项。
|
|
50
|
+
|
|
51
|
+
### 审计与 CI
|
|
52
|
+
|
|
53
|
+
- `audit-manifest.mjs` 新增 4 项:manifest 组件 ⇄ `INSTALL_ORDER` 对账、`pins.nodeEngines` ⇄ `package.json
|
|
54
|
+
engines`、REA pin 探针不得放行下一个 patch/minor、POSIX wrapper 必须标 `executable`。
|
|
55
|
+
- `validate.yml` 新增 `windows-latest` job:PowerShell 下载路线、zip 解压与 bootstrap 此前是零 CI 覆盖的
|
|
56
|
+
Windows 专属代码。
|
|
57
|
+
- 单测 33 条(32 过 + 1 条按平台跳过);`--reuse adopts a compatible node` 在 engines 之外的宿主上改为显式
|
|
58
|
+
skip(此前在 Node 20 上必红,而 CI 钉 22 看不出来)。
|
|
59
|
+
|
|
60
|
+
### 第二轮修复(同一份**更新后**的清单,2026-10-16 晚)
|
|
61
|
+
|
|
62
|
+
- 🔴 **过期 lock 永久卡死自动供给(2.0.0–2.0.2 三版全带)**:`maybeAutoProvision()` 只在 lock 新鲜时
|
|
63
|
+
处理它,过期的 lock 既不清除也不报告 → 落到独占创建 → `EEXIST` → 返回 `in-flight`,而 `main()` 对
|
|
64
|
+
`in-flight` **一个字都不打印**。于是「下载被 kill / 断电」这一常态,在 6 小时后再启动就变成**永久不再
|
|
65
|
+
自动供给且完全静默**。已按清单复现(7 小时死 lock:不启动、lock 原样、无任何提示;5 分钟 lock 对照组
|
|
66
|
+
正常删锁重启),修法:**不新鲜或持有者已死就一律清锁**(这正是注释里"只在新鲜时尊重它"的本意),
|
|
67
|
+
并给 `in-flight` / `spawn-failed` / `unwritable` 补上如实提示。
|
|
68
|
+
- 🔴 **`examples/` 两个文件是 1.x 内容,却随 2.x 进了 npm 包**:`cordis.patch.posix.yml` 教人把整行换成
|
|
69
|
+
`command: rea`、`cordis.patch.with-env.yml` 给的是 `cmd.exe /c rea` 的 1.x 行——对 2.x 全错(2.x 的行是
|
|
70
|
+
跨平台的 `process.execPath` + bundle shim,照抄会整行绕过 bundle)。两份都已是孤儿(README 不再引用),
|
|
71
|
+
故从 `main` **删除**,并同步 `package.json files[]` 与 `validate.yml` 的载荷/YAML/机器路径三项检查
|
|
72
|
+
(v1 分支保留它们,在 1.x 语境里是对的)。npm 载荷由此 21 → 19 个文件。
|
|
73
|
+
- 🟡 **`--offline` 管不住 `reaAgents`**:`provisionReaAgents()` 里 0 处 offline 判断,`npm install` 永远
|
|
74
|
+
联网——与「`--offline` is a promise, not a hint」直接矛盾。现在 `--offline` 且内置副本不可用时**直接
|
|
75
|
+
FAIL** 并说明原因(而可用的内置副本仍照常跳过)。
|
|
76
|
+
- 🟡 **npm 那一步没有代理/镜像兜底**:压缩包有 direct→proxy→PowerShell 三路,npm 只读环境变量。失败时
|
|
77
|
+
现在会打印这句提示,两份 README 的能力/注意里也写明 `HTTPS_PROXY` / `NPM_CONFIG_REGISTRY`。
|
|
78
|
+
- 🟡 **POSIX 缺 `unzip`/`xz` 必现失败**:Ghidra 全平台只有 zip,而 POSIX 解压原先只有 `unzip` 一条路。
|
|
79
|
+
现在 zip 依次尝试 `unzip` → `python3 -m zipfile` → **已供给 JDK 自带的 `jar`**(JDK 在 Ghidra 之前供给,
|
|
80
|
+
等于保底),tar 依次 `tar` → `python3`;三者皆无时打印明确的「no zip extractor available」而不是干巴巴的
|
|
81
|
+
「extraction failed」。
|
|
82
|
+
- 🟡 **manifest 的 arm64 注释是错的**:Google 的 linux/darwin platform-tools 都是 x86_64 二进制,
|
|
83
|
+
「The same archive serves x86-64 and arm64」不成立。三个条目改成如实描述(macOS 需 Rosetta 2、
|
|
84
|
+
linux-arm64 上 probe 预期失败并被报为可选组件不可用)。
|
|
85
|
+
- 🟡 **`docs/bundle-design.md` 与实现漂移**:§3 的「row points at `<plugin>/bin/rea-shim.mjs`」改成
|
|
86
|
+
「按 `DSH_HOME` 算出 `<DSH_HOME>/plugins/rea-bundle/bin/rea-shim.mjs`(镜像副本)」;§7 的
|
|
87
|
+
`vendor/rea-agents` 改成 `vendor/node_modules/rea-agents`。
|
|
88
|
+
- 🟢 **`release.yml` 的 `workflow_dispatch` 可绕过 Release 直接发 npm**:publish job 现在先断言
|
|
89
|
+
`v<package.json version>` 的 tag 在 origin 上存在、且该 Release 挂着版本无关的 `dsh-plugin-rea.tgz`
|
|
90
|
+
资产,否则拒绝发布(否则 npm 与市场兜底链接会岔开)。
|
|
91
|
+
- 🟢 **数值环境变量会变成 NaN**:`REA_BUNDLE_FETCH_RETRIES=abc` → `Math.max(1, NaN)` → 报
|
|
92
|
+
「gave up after NaN attempt(s)」;NaN 的 socket 超时等价于 0(立即超时)。现在非正有限值一律回落到默认,
|
|
93
|
+
且启动日志新增一行 `limits connect=… idle=… retries=…`,让覆盖生效可见。
|
|
94
|
+
- 🟢 注释与整洁:`downloadViaPowerShell` 的文档说自己是 fallback,实际在 Windows 上是主路线(已改口径);
|
|
95
|
+
去掉 setup.mjs 里 3 处对已静态导入成员的重复 `await import("node:fs")`(`readdirSync` 补进静态导入)。
|
|
96
|
+
|
|
5
97
|
## 2.0.2 — 2026-10-15
|
|
6
98
|
|
|
7
99
|
**仅文档,无行为变更。**
|
package/README.md
CHANGED
|
@@ -179,11 +179,11 @@ exact shim launch the MCP row will use.
|
|
|
179
179
|
|
|
180
180
|
## Verified against
|
|
181
181
|
|
|
182
|
-
Author's machine, 2026-10-13 / 14:
|
|
182
|
+
Author's machine, 2026-10-13 / 14 / 16:
|
|
183
183
|
|
|
184
184
|
| Component | Version |
|
|
185
185
|
|---|---|
|
|
186
|
-
| REA (`rea-agents`) | **6.
|
|
186
|
+
| REA (`rea-agents`) | **6.5.0** (207 tools) |
|
|
187
187
|
| DeepSeek Harness | desktop runtime **0.2.0-rc.2**, with its bundled dsh CLI 0.1.7-rc.2 |
|
|
188
188
|
| `@deepseek-ai/dsh-mcp-client` (row provider) | bundled by the harness |
|
|
189
189
|
| Ghidra | 12.1.4 |
|
|
@@ -192,7 +192,7 @@ Author's machine, 2026-10-13 / 14:
|
|
|
192
192
|
| Host | Windows 11 x64 (build 26200) |
|
|
193
193
|
|
|
194
194
|
Exercised live: a clean `setup.mjs --reuse` run reached `setup complete`; an MCP `initialize` handshake
|
|
195
|
-
**through the shim with no `rea` on PATH** answered `serverInfo {name: rea, version: 6.
|
|
195
|
+
**through the shim with no `rea` on PATH** answered `serverInfo {name: rea, version: 6.5.0}`; and the whole
|
|
196
196
|
chain ran under the real desktop host — its own Electron binary launched
|
|
197
197
|
`<DSH_HOME>/plugins/rea-bundle/bin/rea-shim.mjs mcp`, the shim started the **vendored** REA, and a live tool
|
|
198
198
|
call reported `path_source: environment` for a path only that machine's `bundle.json` knows.
|
|
@@ -203,19 +203,28 @@ macOS/Linux.
|
|
|
203
203
|
|
|
204
204
|
## Scope and caveats
|
|
205
205
|
|
|
206
|
-
- REA exposes
|
|
206
|
+
- REA exposes 207 tools at 6.5.0, so every session's tool list grows.
|
|
207
207
|
- Provisioning is honest about checksums: where a vendor publishes one it is pinned and verified; where none
|
|
208
208
|
exists (jadx-headless-mcp, the apktool wrapper, Android platform-tools, the rolling "latest" URLs) the
|
|
209
209
|
hash is **measured once and recorded**, and later drift is reported rather than silently accepted.
|
|
210
210
|
`bundle.json` lists which components are in that state under `unverifiedChecksums`.
|
|
211
211
|
- Downloads are slow on some networks (measured 0.35–0.54 MiB/s from the GitHub release CDN here, so Ghidra
|
|
212
|
-
alone takes ~25–45 min). Fetching is resumable:
|
|
213
|
-
|
|
212
|
+
alone takes ~25–45 min). Fetching is resumable on both routes: a partial `.part` is continued with a Range
|
|
213
|
+
request, and a `.part` that already holds the whole file is verified and kept instead of re-fetched.
|
|
214
214
|
- Some REA tools launch processes, write files and access the network — DSH's permission policy applies.
|
|
215
215
|
Analyse only artifacts you are authorized to inspect.
|
|
216
216
|
- `scripts/setup.mjs` runs `npm install` for the REA CLI with `--ignore-scripts`: the published tarball
|
|
217
217
|
carries no `node_modules`, so a dependency closure must be resolved, and package lifecycle scripts are
|
|
218
|
-
skipped on purpose during provisioning.
|
|
218
|
+
skipped on purpose during provisioning. That step is also the **only** one that cannot use this bundle's
|
|
219
|
+
own route/proxy resolution — npm reads `HTTPS_PROXY`/`HTTP_PROXY` and `NPM_CONFIG_REGISTRY` from the
|
|
220
|
+
environment, so on a network that needs a mirror, set one of those before provisioning
|
|
221
|
+
(`NPM_CONFIG_REGISTRY=https://registry.npmmirror.com`). With `--offline` the step is refused outright
|
|
222
|
+
instead of quietly going online.
|
|
223
|
+
- On POSIX, extraction falls back when the usual tools are missing: a zip is opened with `unzip`, then
|
|
224
|
+
`python3 -m zipfile`, then the provisioned JDK's `jar`; a tarball with `tar`, then `python3`. A minimal
|
|
225
|
+
image that has none of them fails with an explicit "no zip extractor available" line rather than a bare
|
|
226
|
+
"extraction failed". This matters because Ghidra is a zip on every platform and `unzip` is not installed
|
|
227
|
+
everywhere by default.
|
|
219
228
|
|
|
220
229
|
## Updating — three independent things
|
|
221
230
|
|
package/README.zh-CN.md
CHANGED
|
@@ -170,11 +170,11 @@ shim 启动**。
|
|
|
170
170
|
|
|
171
171
|
## 实测环境
|
|
172
172
|
|
|
173
|
-
作者机器,2026-10-13 / 14:
|
|
173
|
+
作者机器,2026-10-13 / 14 / 16:
|
|
174
174
|
|
|
175
175
|
| 组件 | 版本 |
|
|
176
176
|
|---|---|
|
|
177
|
-
| REA(`rea-agents`) | **6.
|
|
177
|
+
| REA(`rea-agents`) | **6.5.0**(207 个工具) |
|
|
178
178
|
| DeepSeek Harness | desktop runtime **0.2.0-rc.2**,内含 dsh CLI 0.1.7-rc.2 |
|
|
179
179
|
| `@deepseek-ai/dsh-mcp-client`(行 provider) | 随 harness 打包 |
|
|
180
180
|
| Ghidra | 12.1.4 |
|
|
@@ -183,7 +183,7 @@ shim 启动**。
|
|
|
183
183
|
| 宿主 | Windows 11 x64(build 26200) |
|
|
184
184
|
|
|
185
185
|
实测跑过:一次干净的 `setup.mjs --reuse` 走到 `setup complete`;一次**不经 PATH 上的 `rea`、完全经由
|
|
186
|
-
shim** 的 MCP `initialize` 握手返回 `serverInfo {name: rea, version: 6.
|
|
186
|
+
shim** 的 MCP `initialize` 握手返回 `serverInfo {name: rea, version: 6.5.0}`;以及**真实桌面宿主下的完整链路** ——
|
|
187
187
|
宿主自己的 Electron 二进制启动 `<DSH_HOME>/plugins/rea-bundle/bin/rea-shim.mjs mcp`,shim 拉起 **bundle 内置**
|
|
188
188
|
的 REA,随后一次真实工具调用报出 `path_source: environment`,而那个路径只有该机的 `bundle.json` 知道。
|
|
189
189
|
|
|
@@ -192,18 +192,27 @@ shim** 的 MCP `initialize` 握手返回 `serverInfo {name: rea, version: 6.4.0}
|
|
|
192
192
|
|
|
193
193
|
## 边界与注意事项
|
|
194
194
|
|
|
195
|
-
- REA 在 6.
|
|
195
|
+
- REA 在 6.5.0 有 207 个工具,每个会话的工具列表都会变长。
|
|
196
196
|
- 校验和对得起良心:厂商发布了的就固定并验证;没发布的(jadx-headless-mcp、apktool 包装脚本、
|
|
197
197
|
Android platform-tools、滚动的 `latest` 地址)则**实测一次并记录**,之后若漂移是**报告**而不是默默接受。
|
|
198
198
|
`bundle.json` 的 `unverifiedChecksums` 会列出处于该状态的组件。
|
|
199
199
|
- 某些网络下很慢(这里实测 GitHub release CDN 只有 0.35–0.54 MiB/s,光 Ghidra 就要 25–45 分钟)。
|
|
200
|
-
|
|
200
|
+
下载是**可续传**的:两条路线都按盘上已有的字节发 Range 继续;若 `.part` 恰好已经完整,会校验后直接收下
|
|
201
|
+
而不是重下。
|
|
201
202
|
- **代理**:Node 的全局 `fetch` 不读系统代理,而 .NET/PowerShell 会读。在开着 WinINET 代理的机器上,
|
|
202
203
|
同一个地址 `Invoke-WebRequest` 通、Node 却 `ECONNRESET`/超时。setup 自己解析代理
|
|
203
204
|
(环境变量优先,Windows 上回退读注册表)并用 CONNECT 隧道,`NO_PROXY` 也照常生效。
|
|
204
205
|
- REA 的部分工具会启动进程、写文件、访问网络 —— DSH 的权限策略照常适用。只分析你有权检视的样本。
|
|
205
206
|
- `scripts/setup.mjs` 为 REA CLI 跑的是带 `--ignore-scripts` 的 `npm install`:官方 tarball 不带
|
|
206
|
-
`node_modules`,必须解析依赖闭包;而供给过程中**刻意跳过包的 lifecycle
|
|
207
|
+
`node_modules`,必须解析依赖闭包;而供给过程中**刻意跳过包的 lifecycle 脚本**。这一步也是**唯一**用不上
|
|
208
|
+
本插件自己的路由/代理回退的环节 —— npm 只从环境读 `HTTPS_PROXY`/`HTTP_PROXY` 与
|
|
209
|
+
`NPM_CONFIG_REGISTRY`,所以网络需要镜像时请先设其中一个(例如
|
|
210
|
+
`NPM_CONFIG_REGISTRY=https://registry.npmmirror.com`)。带 `--offline` 时这一步会被**直接拒绝**,
|
|
211
|
+
而不是悄悄联网。
|
|
212
|
+
- POSIX 上解压有兜底:zip 依次尝试 `unzip` → `python3 -m zipfile` → 已供给 JDK 自带的 `jar`;tar 包依次
|
|
213
|
+
尝试 `tar` → `python3`。三者都没有的最小镜像会得到一句明确的「no zip extractor available」,而不是
|
|
214
|
+
干巴巴的「extraction failed」。Ghidra 在所有平台都只有 zip,而 `unzip` 并非每个发行版默认安装,
|
|
215
|
+
所以这条不是理论问题。
|
|
207
216
|
|
|
208
217
|
## 更新 —— 三件互相独立的事
|
|
209
218
|
|
|
@@ -84,7 +84,7 @@ Draw-Text $g '第三方适配 · 自身不做逆向分析 · 自供给 Node/REA/
|
|
|
84
84
|
$rowY = 180; $boxH = 158; $boxW = 250
|
|
85
85
|
Draw-Box $g 60 $rowY $boxW $boxH $([System.Drawing.Color]::White) $line
|
|
86
86
|
Draw-Text $g 'DeepSeek Harness 会话' $fBox $ink 80 ($rowY + 18)
|
|
87
|
-
Draw-Text $g "mcp__rea__* 工具`n(6.
|
|
87
|
+
Draw-Text $g "mcp__rea__* 工具`n(6.5.0 暴露 207)`n工具随会话注入`n按 DSH 权限策略执行" $fBody $muted 80 ($rowY + 50)
|
|
88
88
|
|
|
89
89
|
Draw-Box $g 360 $rowY $boxW $boxH $([System.Drawing.Color]::FromArgb(255,239,246,255)) $accent
|
|
90
90
|
Draw-Text $g '官方 MCP 客户端行' $fBox $accent 380 ($rowY + 18)
|
|
@@ -112,7 +112,7 @@ Draw-Line $g 358 446 150 446 $muted
|
|
|
112
112
|
Draw-Arrow $g 150 446 150 342 $muted
|
|
113
113
|
|
|
114
114
|
Draw-Text $g '上游 REA(MIT):github.com/morluto/rea 本插件:github.com/czcdr/dsh-plugin-rea' $fSmall $muted 60 620
|
|
115
|
-
Draw-Text $g '实测 REA 6.
|
|
115
|
+
Draw-Text $g '实测 REA 6.5.0 + DSH 桌面运行时 0.2.0-rc.2 + Ghidra 12.1.4 + Node 24.21.0(Windows 11 x64,2026-10-16)' $fSmall $muted 60 646
|
|
116
116
|
Draw-Text $g 'Ghidra 为 Apache-2.0(NSA);REA 为 MIT。本仓库不分发上游代码。' $fSmall $muted 60 672
|
|
117
117
|
Save-Png $bmp $g (Join-Path $OutDir 'screenshot-1.png')
|
|
118
118
|
|
package/assets/screenshot-1.png
CHANGED
|
Binary file
|
package/bin/rea-shim.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* and the message is the operator's remediation.
|
|
16
16
|
*/
|
|
17
17
|
import { spawn } from "node:child_process";
|
|
18
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
19
19
|
import { homedir } from "node:os";
|
|
20
20
|
import path from "node:path";
|
|
21
21
|
import process from "node:process";
|
|
@@ -93,6 +93,40 @@ function provisioningScript(root) {
|
|
|
93
93
|
return usableSetup(PLUGIN_ROOT);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* The provisioning lock is a small JSON record: { state, pid, at, exit? }.
|
|
98
|
+
* `state: "running"` is written when a run is started (setup's own pid once it
|
|
99
|
+
* exists), and the failing run rewrites it as `state: "failed"` — it is kept on
|
|
100
|
+
* purpose so a broken setup cannot be re-downloaded in a loop. A legacy
|
|
101
|
+
* plain-number lock is still honoured as "running".
|
|
102
|
+
*/
|
|
103
|
+
const STALE_LOCK_MS = 6 * 60 * 60 * 1000;
|
|
104
|
+
|
|
105
|
+
function readLock(lock) {
|
|
106
|
+
try {
|
|
107
|
+
const raw = readFileSync(lock, "utf8").trim();
|
|
108
|
+
if (raw.startsWith("{")) {
|
|
109
|
+
const parsed = JSON.parse(raw);
|
|
110
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
111
|
+
}
|
|
112
|
+
const at = Number(raw);
|
|
113
|
+
return Number.isFinite(at) ? { state: "running", at } : null;
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Is the process recorded in the lock still alive? null means "cannot tell". */
|
|
120
|
+
function pidAlive(pid) {
|
|
121
|
+
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
122
|
+
try {
|
|
123
|
+
process.kill(pid, 0);
|
|
124
|
+
return true;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
return error?.code === "ESRCH" ? false : null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
96
130
|
/**
|
|
97
131
|
* On first run there is nothing to launch. Provisioning is a long download, so
|
|
98
132
|
* it is started detached: the harness may kill this row (or the whole session)
|
|
@@ -100,34 +134,58 @@ function provisioningScript(root) {
|
|
|
100
134
|
* a killed attempt is simply continued on the next start.
|
|
101
135
|
*
|
|
102
136
|
* Set REA_BUNDLE_AUTO_PROVISION=0 to require an explicit `setup.mjs` run.
|
|
103
|
-
* Returns
|
|
137
|
+
* Returns { started, reason, logPath } — the caller words its message from
|
|
138
|
+
* `reason`, because "provisioning started in the background" was also printed
|
|
139
|
+
* for a lock left behind by a run that had already failed, and for one whose
|
|
140
|
+
* process was long gone.
|
|
104
141
|
*/
|
|
105
142
|
function maybeAutoProvision(root) {
|
|
106
|
-
if (process.env.REA_BUNDLE_AUTO_PROVISION === "0") return
|
|
143
|
+
if (process.env.REA_BUNDLE_AUTO_PROVISION === "0") return { started: false, reason: "disabled" };
|
|
107
144
|
const setupScript = provisioningScript(root);
|
|
108
|
-
if (!setupScript) return
|
|
145
|
+
if (!setupScript) return { started: false, reason: "no-setup" };
|
|
109
146
|
const lock = path.join(root, "setup.lock");
|
|
110
147
|
const logPath = path.join(root, "logs", "setup.log");
|
|
111
148
|
if (existsSync(lock)) {
|
|
112
149
|
// A crashed run must not wedge provisioning forever: honour the lock only
|
|
113
150
|
// while it is fresh; setup removes it itself when it finishes cleanly.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
151
|
+
const record = readLock(lock);
|
|
152
|
+
const age = record && Number.isFinite(record.at) ? Date.now() - record.at : Infinity;
|
|
153
|
+
const fresh = Number.isFinite(age) && age < STALE_LOCK_MS;
|
|
154
|
+
if (fresh && record?.state === "failed") {
|
|
155
|
+
// The run gave up and left the lock behind so it would not be retried in a
|
|
156
|
+
// loop. Claiming it is still provisioning was a six-hour lie: nothing runs,
|
|
157
|
+
// and every restart repeated the sentence.
|
|
158
|
+
return {
|
|
159
|
+
started: false,
|
|
160
|
+
reason: "last-run-failed",
|
|
161
|
+
exit: record.exit,
|
|
162
|
+
failedAt: record.at,
|
|
163
|
+
ageMs: age,
|
|
164
|
+
logPath,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (fresh) {
|
|
168
|
+
const alive = pidAlive(record?.pid);
|
|
169
|
+
if (alive !== false) return { started: false, reason: "in-flight", logPath, ageMs: age };
|
|
119
170
|
}
|
|
120
|
-
|
|
171
|
+
// Reached when the lock is older than the freshness window, or its holder is
|
|
172
|
+
// gone and never wrote an outcome (harness restart, power loss, a kill): what
|
|
173
|
+
// it records is not happening any more. This removal is the whole point of the
|
|
174
|
+
// freshness rule — without it the exclusive create below failed with EEXIST
|
|
175
|
+
// *forever*, auto-provisioning never ran again, and main() printed nothing at
|
|
176
|
+
// all for that outcome. The lock's own "honour it only while it is fresh"
|
|
177
|
+
// comment described behaviour the code did not have.
|
|
178
|
+
rmSync(lock, { force: true });
|
|
121
179
|
}
|
|
122
180
|
try {
|
|
123
181
|
mkdirSync(path.join(root, "logs"), { recursive: true });
|
|
124
182
|
// Exclusive create: check-then-write is not atomic, so two rows starting at
|
|
125
183
|
// once could both decide to provision. EEXIST means another run owns it.
|
|
126
|
-
writeFileSync(lock,
|
|
184
|
+
writeFileSync(lock, JSON.stringify({ state: "running", pid: process.pid, at: Date.now() }), { flag: "wx" });
|
|
127
185
|
} catch (error) {
|
|
128
|
-
if (error?.code === "EEXIST") return logPath;
|
|
186
|
+
if (error?.code === "EEXIST") return { started: false, reason: "in-flight", logPath };
|
|
129
187
|
/* a read-only bundle root must never break the harness */
|
|
130
|
-
return
|
|
188
|
+
return { started: false, reason: "unwritable" };
|
|
131
189
|
}
|
|
132
190
|
try {
|
|
133
191
|
const child = spawn(process.execPath, [setupScript, "--root", root], {
|
|
@@ -136,9 +194,18 @@ function maybeAutoProvision(root) {
|
|
|
136
194
|
windowsHide: true,
|
|
137
195
|
});
|
|
138
196
|
child.unref();
|
|
139
|
-
|
|
197
|
+
// Hand the lock to the detached downloader, so a later start that finds this
|
|
198
|
+
// row dead still sees the run that is actually doing the work as alive.
|
|
199
|
+
if (Number.isInteger(child.pid)) {
|
|
200
|
+
try {
|
|
201
|
+
writeFileSync(lock, JSON.stringify({ state: "running", pid: child.pid, at: Date.now(), supervisor: process.pid }));
|
|
202
|
+
} catch {
|
|
203
|
+
/* the lock is advisory: the row keeps working without it */
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return { started: true, reason: "started", logPath };
|
|
140
207
|
} catch {
|
|
141
|
-
return
|
|
208
|
+
return { started: false, reason: "spawn-failed", logPath };
|
|
142
209
|
}
|
|
143
210
|
}
|
|
144
211
|
|
|
@@ -195,8 +262,13 @@ function readBundleConfig(root) {
|
|
|
195
262
|
function resolveRea(config, root) {
|
|
196
263
|
const paths = config.paths ?? {};
|
|
197
264
|
const node = paths.node;
|
|
265
|
+
// The fallback has to match where setup actually installs the package:
|
|
266
|
+
// `npm install --prefix <root>/vendor` resolves into vendor/node_modules, so a
|
|
267
|
+
// fallback without that segment named a directory that never exists. It only
|
|
268
|
+
// fires when an older bundle.json has no paths.reaEntry — which is exactly when
|
|
269
|
+
// a wrong fallback turns a usable install into "the vendored REA CLI is gone".
|
|
198
270
|
const reaEntry = paths.reaEntry
|
|
199
|
-
?? path.join(root, "vendor", "rea-agents", "scripts", "rea.mjs");
|
|
271
|
+
?? path.join(root, "vendor", "node_modules", "rea-agents", "scripts", "rea.mjs");
|
|
200
272
|
|
|
201
273
|
if (typeof node !== "string" || node.trim() === "") {
|
|
202
274
|
return { ok: false, reason: "bundle.json has no paths.node" };
|
|
@@ -302,10 +374,31 @@ function main() {
|
|
|
302
374
|
loaded.code === "missing"
|
|
303
375
|
? `No bundle state at ${loaded.file}.`
|
|
304
376
|
: `Bundle state at ${loaded.file} could not be parsed.`;
|
|
305
|
-
|
|
306
|
-
|
|
377
|
+
// Both "missing" and "malformed" start a run. A malformed file used to fail
|
|
378
|
+
// permanently: setup overwrites a bundle.json it cannot parse, but nothing
|
|
379
|
+
// ever started setup, so every restart reported the same EX_CONFIG until the
|
|
380
|
+
// operator deleted the file by hand. The lock stops a provisioning loop when
|
|
381
|
+
// writing keeps failing.
|
|
382
|
+
const provisioning = maybeAutoProvision(root);
|
|
383
|
+
if (provisioning.started) {
|
|
384
|
+
process.stderr.write(
|
|
385
|
+
`\n dsh-plugin-rea (bundle): first run — provisioning started in the background.\n progress: ${provisioning.logPath}\n`,
|
|
386
|
+
);
|
|
387
|
+
} else if (provisioning.reason === "last-run-failed") {
|
|
388
|
+
process.stderr.write(
|
|
389
|
+
`\n dsh-plugin-rea (bundle): the last provisioning attempt failed (exit ${provisioning.exit ?? "?"}).\n` +
|
|
390
|
+
` Nothing is running right now — read the log, then re-run setup.\n log: ${provisioning.logPath}\n`,
|
|
391
|
+
);
|
|
392
|
+
} else if (provisioning.reason === "in-flight") {
|
|
393
|
+
// A live run owns the lock. Saying nothing here is what made a wedged
|
|
394
|
+
// provisioning attempt look like a machine that simply has no bundle.
|
|
395
|
+
process.stderr.write(
|
|
396
|
+
`\n dsh-plugin-rea (bundle): provisioning is already running in the background.\n progress: ${provisioning.logPath}\n`,
|
|
397
|
+
);
|
|
398
|
+
} else if (provisioning.reason === "spawn-failed" || provisioning.reason === "unwritable") {
|
|
307
399
|
process.stderr.write(
|
|
308
|
-
`\n dsh-plugin-rea (bundle):
|
|
400
|
+
`\n dsh-plugin-rea (bundle): provisioning could not be started (${provisioning.reason}).\n` +
|
|
401
|
+
` Run it by hand:\n node "${path.join(root, "scripts", "setup.mjs")}"\n`,
|
|
309
402
|
);
|
|
310
403
|
}
|
|
311
404
|
return fail(message, loaded.detail);
|
package/check-updates.ps1
CHANGED
|
@@ -69,13 +69,40 @@ function Test-NodeEngines([string]$node, [string]$engines) {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
# ------------------------------------------------------- 定位 REA 与 profile
|
|
72
|
+
# 2.x 的 REA 在 <bundleRoot>\vendor\node_modules\rea-agents,不在 npm 全局。先读
|
|
73
|
+
# bundle.json:把全局路径当默认是 1.x 的答案,在 2.x 机器上会让这一节指向错误的
|
|
74
|
+
# 对象("全局包丢了"),并给出错误的升级动作(npm i -g)。
|
|
75
|
+
$bundleRootEarly = $env:REA_BUNDLE_ROOT
|
|
76
|
+
if (-not $bundleRootEarly) {
|
|
77
|
+
$homeEarly = $env:DSH_HOME
|
|
78
|
+
if (-not $homeEarly) { $homeEarly = Join-Path $HOME '.dsh' }
|
|
79
|
+
$bundleRootEarly = Join-Path $homeEarly 'plugins\rea-bundle'
|
|
80
|
+
}
|
|
81
|
+
$bundleState = $null
|
|
82
|
+
$bundleJsonEarly = Join-Path $bundleRootEarly 'bundle.json'
|
|
83
|
+
if (Test-Path $bundleJsonEarly) {
|
|
84
|
+
try { $bundleState = Get-Content $bundleJsonEarly -Raw -Encoding UTF8 | ConvertFrom-Json } catch { $bundleState = $null }
|
|
85
|
+
}
|
|
86
|
+
$bundleReaPackageDir = $null
|
|
87
|
+
$bundleReaEntry = $null
|
|
88
|
+
if ($bundleState -and $bundleState.paths.reaEntry) {
|
|
89
|
+
$bundleReaEntry = [string]$bundleState.paths.reaEntry
|
|
90
|
+
# <vendor>\node_modules\rea-agents\scripts\rea.mjs -> <vendor>\node_modules\rea-agents
|
|
91
|
+
$bundleReaPackageDir = Split-Path -Parent (Split-Path -Parent $bundleReaEntry)
|
|
92
|
+
}
|
|
93
|
+
|
|
72
94
|
if (-not $ReaPackageDir) {
|
|
73
|
-
$
|
|
74
|
-
|
|
75
|
-
|
|
95
|
+
if ($bundleReaPackageDir -and (Test-Path (Join-Path $bundleReaPackageDir 'package.json'))) {
|
|
96
|
+
$ReaPackageDir = $bundleReaPackageDir
|
|
97
|
+
} else {
|
|
98
|
+
$npmRoot = $null
|
|
99
|
+
if (Get-Command npm -ErrorAction SilentlyContinue) {
|
|
100
|
+
try { $npmRoot = (& npm root -g 2>$null | Select-Object -First 1).Trim() } catch { }
|
|
101
|
+
}
|
|
102
|
+
$ReaPackageDir = if ($npmRoot) { Join-Path $npmRoot 'rea-agents' } else { Join-Path $env:APPDATA 'npm\node_modules\rea-agents' }
|
|
76
103
|
}
|
|
77
|
-
$ReaPackageDir = if ($npmRoot) { Join-Path $npmRoot 'rea-agents' } else { Join-Path $env:APPDATA 'npm\node_modules\rea-agents' }
|
|
78
104
|
}
|
|
105
|
+
$reaFromBundle = [bool]($bundleReaPackageDir -and $ReaPackageDir -eq $bundleReaPackageDir)
|
|
79
106
|
if (-not $ProfileDir) {
|
|
80
107
|
if ($env:DSH_PROFILE_DIR) { $ProfileDir = $env:DSH_PROFILE_DIR }
|
|
81
108
|
else {
|
|
@@ -94,11 +121,21 @@ if (Test-Path $pkgFile) {
|
|
|
94
121
|
$installed = $pkg.version; $engines = $pkg.engines.node
|
|
95
122
|
$binPath = ($pkg.bin.PSObject.Properties | Select-Object -First 1).Value
|
|
96
123
|
if ($binPath) { $reaEntry = Join-Path $ReaPackageDir ($binPath -replace '/', '\') }
|
|
97
|
-
|
|
98
|
-
|
|
124
|
+
if ($reaFromBundle) {
|
|
125
|
+
Ok "一体包内置(vendored): $installed"
|
|
126
|
+
Info "位置: $ReaPackageDir"
|
|
127
|
+
Info "入口(bin): $binPath"
|
|
128
|
+
} else {
|
|
129
|
+
Ok "本地已安装: $installed"
|
|
130
|
+
Info "入口(bin): $binPath ← 插件通过 PATH 上的 rea 调用它,跨版本稳定"
|
|
131
|
+
}
|
|
99
132
|
if ($engines) { Info "Node 要求: $engines" }
|
|
100
133
|
} catch { Bad "package.json 解析失败: $($_.Exception.Message)" }
|
|
101
|
-
} else {
|
|
134
|
+
} else {
|
|
135
|
+
if ($reaFromBundle) { Bad "找不到 $pkgFile —— 内置副本不完整?重跑 setup.mjs" }
|
|
136
|
+
elseif ($bundleReaPackageDir) { Bad "找不到 $pkgFile —— 一体包内置副本也缺($bundleReaPackageDir);记录里没有 reaEntry" }
|
|
137
|
+
else { Bad "找不到 $pkgFile —— REA 全局包丢了?重装: npm i -g rea-agents" }
|
|
138
|
+
}
|
|
102
139
|
|
|
103
140
|
$latest = $null
|
|
104
141
|
foreach ($r in @($Registry, 'https://registry.npmjs.org')) {
|
|
@@ -110,7 +147,11 @@ if ($installed -and $latest) {
|
|
|
110
147
|
$vi = Get-NumericVersion $installed; $vl = Get-NumericVersion $latest
|
|
111
148
|
if ($vi -and $vl -and $vl -gt $vi) {
|
|
112
149
|
Warn "有新版本:$installed → $latest"
|
|
113
|
-
$
|
|
150
|
+
if ($reaFromBundle) {
|
|
151
|
+
$actions.Add("升级一体包内置 REA:把 manifest 的 pins.reaAgents / components.reaAgents.sources.npm.version 改成 $latest,然后重跑 node scripts\setup.mjs(内置副本不会自己更新)")
|
|
152
|
+
} else {
|
|
153
|
+
$actions.Add("升级上游 REA: npm i -g rea-agents@$latest (只更新 REA;本插件不会自动更新——从市场装的在市场里更新,file: 装的覆盖目录后重装)")
|
|
154
|
+
}
|
|
114
155
|
} elseif ($vi -and $vl -and $vl -eq $vi -and $installed -ne $latest) {
|
|
115
156
|
Warn "同版本号但通道不同:$installed → $latest(预发布/正式)"
|
|
116
157
|
} else { Ok '已是最新版本' }
|
|
@@ -139,7 +180,14 @@ if (Get-Command node -ErrorAction SilentlyContinue) {
|
|
|
139
180
|
|
|
140
181
|
# ------------------------------------------------------------ Ghidra 引擎
|
|
141
182
|
Head 'Ghidra 引擎'
|
|
142
|
-
|
|
183
|
+
# bundle.json 是 2.x 的真源:shim 只在 MCP 子进程里设 GHIDRA_INSTALL_DIR,用户级
|
|
184
|
+
# 变量是 1.x 的写法,没手工设过的机器会被误报"未设置"。
|
|
185
|
+
$gd = $null
|
|
186
|
+
if ($bundleState -and $bundleState.paths.ghidra) {
|
|
187
|
+
$bundleGhidra = [string]$bundleState.paths.ghidra
|
|
188
|
+
if (Test-Path $bundleGhidra) { $gd = $bundleGhidra }
|
|
189
|
+
}
|
|
190
|
+
if (-not $gd) { $gd = [Environment]::GetEnvironmentVariable('GHIDRA_INSTALL_DIR', 'User') }
|
|
143
191
|
if (-not $gd) { $gd = $env:GHIDRA_INSTALL_DIR }
|
|
144
192
|
if ($gd -and (Test-Path $gd)) {
|
|
145
193
|
Ok "GHIDRA_INSTALL_DIR = $gd"
|
|
@@ -162,11 +210,28 @@ try {
|
|
|
162
210
|
# --------------------------------------------------------- REA 引擎自检
|
|
163
211
|
Head 'REA 引擎自检(rea doctor)'
|
|
164
212
|
$env:GHIDRA_INSTALL_DIR = $gd
|
|
165
|
-
#
|
|
166
|
-
#
|
|
167
|
-
$reaCmd =
|
|
168
|
-
if (-not $reaCmd) { $reaCmd = (Get-Command rea -ErrorAction SilentlyContinue).Source }
|
|
213
|
+
# 2.x:优先用 bundle 内置的 CLI 和它自己的 Node。PATH 上的 rea 可能是另一个,
|
|
214
|
+
# 甚至完全不在 PATH —— 那会让整段自检被静默跳过。这正是 1.x 口径的残留。
|
|
215
|
+
$reaCmd = $null
|
|
169
216
|
$reaArgs = @()
|
|
217
|
+
if ($bundleReaEntry -and (Test-Path $bundleReaEntry)) {
|
|
218
|
+
$bundleNode = $null
|
|
219
|
+
if ($bundleState -and $bundleState.paths.node) {
|
|
220
|
+
$candidateNode = [string]$bundleState.paths.node
|
|
221
|
+
if (Test-Path $candidateNode) { $bundleNode = $candidateNode }
|
|
222
|
+
}
|
|
223
|
+
if (-not $bundleNode) { $bundleNode = (Get-Command node -ErrorAction SilentlyContinue).Source }
|
|
224
|
+
if ($bundleNode) {
|
|
225
|
+
$reaCmd = $bundleNode; $reaArgs = @($bundleReaEntry)
|
|
226
|
+
Info '使用一体包内置 REA(node <bundle 入口>),不依赖 PATH 上的 rea'
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (-not $reaCmd) {
|
|
230
|
+
# 优先取 .cmd:发布出去的那一行是 `cmd.exe /c rea mcp`(走 cmd 的 PATHEXT,命中 rea.cmd),
|
|
231
|
+
# 而 PowerShell 的 Get-Command rea 会先命中同目录的 rea.ps1 垫片,两者行为(尤其退出码)并不相同。
|
|
232
|
+
$reaCmd = (Get-Command 'rea.cmd' -ErrorAction SilentlyContinue).Source
|
|
233
|
+
if (-not $reaCmd) { $reaCmd = (Get-Command rea -ErrorAction SilentlyContinue).Source }
|
|
234
|
+
}
|
|
170
235
|
if (-not $reaCmd) {
|
|
171
236
|
if ($reaEntry -and (Test-Path $reaEntry) -and (Get-Command node -ErrorAction SilentlyContinue)) {
|
|
172
237
|
$reaCmd = 'node'; $reaArgs = @($reaEntry)
|
|
@@ -247,6 +312,8 @@ if (-not (Test-Path $manifestPath)) {
|
|
|
247
312
|
$state = Get-Content $bundleJson -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
248
313
|
Ok "已供给:$($state.provisionedAt)"
|
|
249
314
|
$drift = 0
|
|
315
|
+
$compared = 0
|
|
316
|
+
$unrecorded = 0
|
|
250
317
|
foreach ($name in $manifest.pins.PSObject.Properties.Name) {
|
|
251
318
|
if ($name -eq 'nodeEngines') { continue }
|
|
252
319
|
$pinned = [string]$manifest.pins.$name
|
|
@@ -254,13 +321,21 @@ if (-not (Test-Path $manifestPath)) {
|
|
|
254
321
|
if ($state.versions -and ($state.versions.PSObject.Properties.Name -contains $name)) {
|
|
255
322
|
$installed = [string]$state.versions.$name
|
|
256
323
|
}
|
|
257
|
-
if (-not $installed) {
|
|
324
|
+
if (-not $installed) {
|
|
325
|
+
# 被 --reuse 采纳的组件没有 version 字段(bundle.json 里是 null)。静默
|
|
326
|
+
# continue 会让这一项从未被比对,却在下面打印"全部一致"——那是假绿。
|
|
327
|
+
$unrecorded++
|
|
328
|
+
Warn " $name 未记录版本(reuse 采纳的组件?)—— 无法与 pin $pinned 对账"
|
|
329
|
+
continue
|
|
330
|
+
}
|
|
331
|
+
$compared++
|
|
258
332
|
# Node 的 pin 是引擎范围,不是版本号;其它组件比前缀即可
|
|
259
333
|
$same = $installed.Equals($pinned) -or $installed.StartsWith(($pinned -split '\.')[0] + '.')
|
|
260
334
|
if ($same) { Info " $name = $installed(pin $pinned)" }
|
|
261
335
|
else { Warn "$name 盘上为 $installed,manifest 已钉 $pinned —— 重跑 setup 对齐"; $drift++ }
|
|
262
336
|
}
|
|
263
|
-
if ($drift -eq 0) { Ok '所有组件与 manifest 的 pin 一致' }
|
|
337
|
+
if ($drift -eq 0 -and $unrecorded -eq 0) { Ok '所有组件与 manifest 的 pin 一致' }
|
|
338
|
+
elseif ($drift -eq 0) { Warn "已比对的 $compared 项与 pin 一致,但 $unrecorded 项没有记录版本(见上)—— 不是全绿" }
|
|
264
339
|
if ($state.unverifiedChecksums -and @($state.unverifiedChecksums).Count -gt 0) {
|
|
265
340
|
Warn ("TOFU(无厂商校验和,取首次实测值): " + (@($state.unverifiedChecksums) -join ', '))
|
|
266
341
|
}
|
package/docs/bundle-design.md
CHANGED
|
@@ -31,11 +31,11 @@ Two upstream rules shape the runtime wiring:
|
|
|
31
31
|
|
|
32
32
|
## 2. Verified facts this design rests on
|
|
33
33
|
|
|
34
|
-
Measured on Windows 11 x64, REA 6.
|
|
34
|
+
Measured on Windows 11 x64, REA 6.5.0, Ghidra 12.1.4, Temurin JDK 25.0.4:
|
|
35
35
|
|
|
36
36
|
| Fact | Evidence |
|
|
37
37
|
| --- | --- |
|
|
38
|
-
| A row can launch REA with **no PATH `rea`** | `node <pkg>/scripts/rea.mjs mcp` answered the MCP `initialize` handshake (`serverInfo.name = rea`, version 6.
|
|
38
|
+
| A row can launch REA with **no PATH `rea`** | `node <pkg>/scripts/rea.mjs mcp` answered the MCP `initialize` handshake (`serverInfo.name = rea`, version 6.5.0) and `rea doctor --json` reported 207 tools. `healthy` is `false` on a bundle-only install: 6.5.0 also checks the host's own setup (Hopper, IDA registration, `rea setup` skill/registrations), which provisioning deliberately never touches — the row does not require `healthy`, only a CLI that runs and a catalog it can read |
|
|
39
39
|
| `!!js` expressions are supported in this profile layer | the existing profile patch already used one; the loader evaluates them |
|
|
40
40
|
| A row-level `env:` block reaches the MCP child | `binary_session` reported `registrationEnvironment` = `GHIDRA_INSTALL_DIR`, `JAVA_HOME` |
|
|
41
41
|
| No fixed home/layout is promised | the disk layout differs per install; nothing may assume one |
|
|
@@ -54,8 +54,9 @@ Three responsibilities, kept strictly apart:
|
|
|
54
54
|
checksums, unpacks, runs the readiness probe, and writes `bundle.json`.
|
|
55
55
|
Idempotent and re-runnable; `--reuse` adopts pre-existing installs.
|
|
56
56
|
3. **Patch (wiring, published).** `cordis.patch.yml` on `main`, carrying
|
|
57
|
-
**no machine path**: the row
|
|
58
|
-
shim is responsible for the
|
|
57
|
+
**no machine path**: the row computes `<DSH_HOME>/plugins/rea-bundle/bin/rea-shim.mjs`
|
|
58
|
+
at load time from `DSH_HOME` alone, and that mirrored shim is responsible for the
|
|
59
|
+
rest. (The checkout's own copy is what a human runs; the row never points at it.)
|
|
59
60
|
|
|
60
61
|
The published plugin therefore stays portable, while the machine-specific truth
|
|
61
62
|
lives in one small JSON file under the DSH home.
|
|
@@ -161,7 +162,7 @@ Windows and is what this bundle targets to complete.
|
|
|
161
162
|
| Component | Needed for | Provisioned as |
|
|
162
163
|
| --- | --- | --- |
|
|
163
164
|
| Node runtime | running REA at all | reuse a compatible existing node, else portable `node.exe` |
|
|
164
|
-
| `rea-agents` | the MCP server itself | vendored copy under `vendor/rea-agents` |
|
|
165
|
+
| `rea-agents` | the MCP server itself | vendored copy under `vendor/node_modules/rea-agents` |
|
|
165
166
|
| Temurin JDK 21/25 | Ghidra headless + JADX bridge | portable JDK zip (full JDK, not a JRE) |
|
|
166
167
|
| Ghidra 12.1.x | native analysis | official release zip, extracted |
|
|
167
168
|
| apktool | Android resource decode | `apktool.bat` + jar in one directory |
|