mirror of
https://github.com/czcdr/dsh-plugin-rea.git
synced 2026-11-07 10:25:59 -10:00
fix: record what was adopted, smoke the engine, pin Node 24.22.1 (2.0.11)
setup.mjs — three defects that all made `bundle.json` describe something other
than what the machine runs:
* A reused component carried no version. `provisionComponent` rebuilt
`{path, reused}` from the adopted record, so `versions.node` fell back to the
interpreter running setup and `versions.jdk`/`versions.ghidra` to the
manifest pin. Both are wrong whenever the adopted build is not that one — the
exact case `check-updates.ps1` exists to catch. Each installation is now asked
what it is: `-v` for Node, `release` for a JDK, `Ghidra/application.properties`
for Ghidra.
* The provisioning-time `rea doctor` got neither `GHIDRA_INSTALL_DIR` nor
`JAVA_HOME`, so REA fell back to probing the machine and reported whichever
Ghidra and JDK were installed globally. On this host that was a different
Ghidra than the one being provisioned. Doctor and the smoke step now receive
the same two variables the mirrored shim exports.
* New `smoke` step: after wiring, import a small PE through `rea analyze` and
fail the run when it cannot. `doctor` validates paths, versions and which Java
would be chosen but never imports a binary, so a broken bridge or a mismatched
native addon surfaced only as an MCP tool timeout on the first real analysis —
which reads exactly like "the runtime upgrade broke REA". Measured here: 36 s
for a small PE, 64 s for a 6 MB DLL. `--skip-smoke` omits it.
manifest — Node 24.21.0 -> 24.22.1 on all six platforms (version, url, size,
sha256, verification date). 24.22.1 is Node's 2026-10-21 security release
(Brotli decoder out-of-bounds write RCE, two TLS hostname bypasses, eMudhra
intermediate removal); the reuse path was fine on a machine that already had it,
but a clean environment downloaded the vulnerable 24.21.0. Checksums come from
the official SHASUMS256.txt; the win32-x64 archive was also downloaded, hashed,
unpacked and run (`node -v`, `npm -v`) here.
scripts/check-updates.mjs — `npm run check-updates` goes through a portable
wrapper: PowerShell (5.1 first, then pwsh) on Windows, and on other platforms an
explicit explanation plus the two commands that do work, exiting non-zero rather
than pretending "not applicable" is "all green".
Tests 81 -> 85: smoke is skipped without an engine, --skip-smoke wins, the smoke
step really drives `analyze` and a failure is a FAIL, and the reuse record
spreads the adopted version instead of rebuilding it.
This commit is contained in:
1 parent
1438672f9a
commit
50864f0a9d
9 files changed
+465
-80
No files matched your search
+28
-4
@@ -17,6 +17,11 @@
|
||||
- 点「稍后」或「我已运行」后按**插件版本**记入 localStorage:同一版本不再弹,升级换了版本后重新提示一次;
|
||||
- 该 route 只在宿主有 `webServer` 时挂载(`ctx.inject(['webServer'])`),headless/ACP 主机行为不变;
|
||||
- 它只是提示:不 spawn、不下载、不改 `bundle.json`,也不跑 setup。
|
||||
- **`setup.mjs` 末尾新增 Ghidra 冒烟(`smoke` 步,`--skip-smoke` 可关)**:`rea doctor` 只校验路径、版本与
|
||||
会用哪个 Java,**从不导入一个二进制**。首次真实导入要付一次性成本(本机实测:小 PE 约 36 s,6 MB DLL 约 64 s),
|
||||
一旦桥接或 native addon 有问题,操作者只在**第一次 MCP 工具调用超时**时才撞见 —— 那读起来就是「升级把 REA 搞坏了」。
|
||||
现在供给结束会用 JDK 自带的 `bin/java` 跑一次 `rea analyze --json`,把「能不能分析」变成有测量结果的结论;
|
||||
失败即 `FAIL` 并计入退出码。`--skip-smoke` 跳过(CI 或低内存机器)。
|
||||
|
||||
### 修复
|
||||
|
||||
@@ -38,6 +43,23 @@
|
||||
- **`foreignReaRegistration` 不剥行尾注释**:`serverName: rea # upstream` 会被解析成 `rea # upstream` ≠ `rea`,
|
||||
重复注册漏报 —— 而这正是人手编辑那个文件时最常见的形态。现在按 YAML 规则取值:引号内原样,引号外 ` #` 之后是
|
||||
注释(`rea#1` 仍然是值)。
|
||||
- **`npm run check-updates` 在非 Windows 上是坏的**:这个 script 直接写死了
|
||||
`powershell -NoProfile -File ./check-updates.ps1`,而脚本本身读 `%APPDATA%`、优先 `rea.cmd`
|
||||
(并刻意避开 `rea.ps1` 垫片),所以在 macOS/Linux 上即使装了 `pwsh` 也只会从一个不存在的布局里
|
||||
给出一个自信的错答案。现在改为 `node scripts/check-updates.mjs`:Windows 上转发给 `powershell`
|
||||
(缺失时退 `pwsh`),其它平台说明原因、列出 `node bin/rea-shim.mjs --shim-status` 与
|
||||
`node scripts/setup.mjs --dry-run`,并以**非零码**退出 —— 不把「不适用」伪装成「全绿」。
|
||||
- **供给期的 `rea doctor` 检查的不是本次供给的引擎**:那一步没传 `GHIDRA_INSTALL_DIR` / `JAVA_HOME`,
|
||||
REA 于是回落到探测本机,于是报告里出现的是系统那份 Ghidra 与 JDK(本机实测:报的是 `D:\ghidra\...` + 系统
|
||||
Java 25,而 vendor 里那份从未被检查)。现在 doctor 与冒烟共用与镜像 shim 完全相同的一组变量。
|
||||
- **复用 Node 时不记录候选的真实版本**:`reusableNodeChecked` 只回 `{ok}`,于是 `bundle.json` 的
|
||||
`versions.node` 落到 `process.versions.node` —— 那是**跑 setup 的解释器**,不是被复用的那个。两者不同时
|
||||
(`REA_BUNDLE_NODE`,或 PATH 首个 `node` 是另一个安装)记录与实际就静默不一致。现在探针把 `-v` 的第一行
|
||||
带回,复用路径照实记录。
|
||||
- **manifest 把 Node 钉在 24.21.0(含已知漏洞)**:24.22.1 是 2026-10-21 的安全发布(Brotli 解码器越界写 RCE、
|
||||
两处 TLS 主机名绕过、移除 eMudhra 中间 CA)。复用路径在装有新 Node 的机器上没问题,但**干净环境走下载路径
|
||||
会装回带漏洞的 24.21.0**。六个平台的 `version` / `url` / `size` / `sha256` 全部升到 24.22.1
|
||||
(校验和取自官方 `SHASUMS256.txt`;win32-x64 在本机下载、比对、解压并跑通 `node -v` / `npm -v` 后写入)。
|
||||
|
||||
### 升级
|
||||
|
||||
@@ -56,11 +78,13 @@
|
||||
|
||||
### 验证
|
||||
|
||||
- 单测 **78 条**(新增「UTF-16 的 `bundle.json` 被 setup 读到」「UTF-16 的 `bundle.json` 被 shim 读到」两条;
|
||||
重复注册用例扩成「带行尾注释」;「setup 指南」5 条:钉子精确/家族比对、无记录=首装、旧 REA=stale、UTF-16 记录、
|
||||
坏记录/无 REA 字段都要求 setup)。
|
||||
- 单测 **84 条**(新增「冒烟在没有引擎时跳过」「`--skip-smoke` 直接关闭」「冒烟必须真调 `analyze` 且失败即
|
||||
`FAIL`、探针与冒烟共用 vendor 变量」三条;此前 81 条:UTF-16 的 `bundle.json` 被 setup / shim 读到、「setup 指南」
|
||||
5 条、「check-updates 包装器」3 条)。
|
||||
- `manifest` 审计:**24 条 fetch 全部 hash-pinned,0 TOFU**(Node 六平台换到 24.22.1 后重跑)。
|
||||
- `verify-branch.ps1` 新增 §9:指南链路四端(`bin/rea-guide.mjs` 的 route、`index.mjs` 的挂载、`client.js` 的
|
||||
fetch、`cordis.patch.yml` 的行)逐一比对,防止改名后指南静默不出现。
|
||||
fetch、`cordis.patch.yml` 的行)逐一比对,防止改名后指南静默不出现;§10:`npm run check-updates` 必须走
|
||||
包装器,且包装器必须保留「Windows 才跑」与两条替代命令。
|
||||
- 未发版:无 tag / 无 Release / npm 仍 **2.0.10**。
|
||||
|
||||
## 2.0.10 — 2026-10-19
|
||||
|
||||
@@ -204,6 +204,10 @@ Three details in that row are load-bearing, and all three were bugs first:
|
||||
- **The `!!js` scalar is quoted.** A bare scalar containing `? ` and `: ` is not one scalar: YAML reads it as an explicit key/value pair, so the loader receives a *mapping* where an expression belongs. Measured against the harness's own dialect (`JSON_SCHEMA` plus the `!!js` type from `app.asar`), the unquoted form made `args[0]` into `{"[object Object]": "..."}` — an object the row cannot launch with. Quoting removes the ambiguity, and the JS string literals inside then use single quotes.
|
||||
- **Forward slashes, never backslashes.** A single-quoted YAML scalar keeps `\` literal, and JavaScript then consumes it: `'\plugins'` is not a path but the BEL control character, `'\rea-bundle'` becomes a carriage return and `'\bin'` a backspace — the path is destroyed silently. Forward slashes work on Windows and on POSIX.
|
||||
- **The `DSH_HOME` fallback is `<home>/.dsh`, not `<home>`.** The shim, `setup.mjs` and `bootstrap.ps1` all resolve an unset `DSH_HOME` to `<home>/.dsh/...`; a fallback that stopped one level higher made the row point at a file provisioning never writes — invisible on any machine where `DSH_HOME` happens to be set.
|
||||
- **With `DSH_HOME`, `USERPROFILE` and `HOME` all empty, the expression resolves to `/.dsh/...`** — the one
|
||||
combination the concatenation cannot improve on. That is a service or container account with no home
|
||||
directory; the row then cannot start, and because `failOnStartupError: false` keeps that quiet, the only
|
||||
symptom is "no `mcp__rea__*` tools". Set `DSH_HOME` explicitly on such an account.
|
||||
- **No `path`/`require` in a `!!js` expression**: `ctx` is the loader scope, so the DSH_HOME fallback is written as plain concatenation.
|
||||
|
||||
`npm run bundle:audit` enforces the first three: it evaluates this exact expression with `DSH_HOME` unset, empty and set, and rejects a bare scalar that YAML would read as a mapping.
|
||||
@@ -268,12 +272,20 @@ Ask for a `mcp__rea__*` call — `binary_session` reports the engine checks
|
||||
(`configuration` / `installation` / `headless` / `native_decompiler` / `java`), all of which should be
|
||||
`ok: true`. Then `open_binary` a PE, `binary_overview`, and `procedure_pseudo_code`.
|
||||
|
||||
Provisioning itself ends with two probes: `rea doctor --json` (reported, with unmet checks named) and the
|
||||
exact shim launch the MCP row will use.
|
||||
Provisioning itself ends with three checks: `rea doctor --json` (reported, with unmet checks named), a
|
||||
**Ghidra smoke import** that runs `rea analyze` on the JDK's own `bin/java` and fails the run when it
|
||||
cannot import (`--skip-smoke` omits it), and the exact shim launch the MCP row will use. The smoke step
|
||||
exists because `doctor` validates paths, versions and which Java would be chosen — it never imports a
|
||||
binary, so a broken bridge or a mismatched native addon used to surface only as an MCP timeout.
|
||||
|
||||
**The first analysis of each new target is slow.** Measured here: 36 s for a small PE, 64 s for a 6 MB
|
||||
DLL — every import starts a JVM and runs Ghidra's default auto-analysis. If the first
|
||||
`open_binary` → `list_segments` on a large file times out, that is this cost and not a broken engine:
|
||||
call it again and the open target answers immediately, because it keeps its ephemeral project.
|
||||
|
||||
## Verified against
|
||||
|
||||
Author's machine, 2026-10-13 / 14 / 16 / 19:
|
||||
Author's machine, 2026-10-13 / 14 / 16 / 19 / 23:
|
||||
|
||||
| Component | Version |
|
||||
|---|---|
|
||||
@@ -281,8 +293,8 @@ Author's machine, 2026-10-13 / 14 / 16 / 19:
|
||||
| DeepSeek Harness | desktop runtime **0.2.0-rc.2**, with its bundled dsh CLI 0.1.7-rc.2 |
|
||||
| `@deepseek-ai/dsh-mcp-client` (row provider) | bundled by the harness |
|
||||
| Ghidra | 12.1.4 |
|
||||
| JDK | Temurin 21 (provisioned) / Microsoft OpenJDK 25.0.4 (adopted) |
|
||||
| Node.js | 24.21.0 (REA engines: `^22.19.0 \|\| ^24.11.0 \|\| >=26.0.0`) |
|
||||
| JDK | Temurin 21 (provisioned) / Amazon Corretto 25.0.5 (adopted by the host) |
|
||||
| Node.js | **24.22.1** adopted (REA engines: `^22.19.0 \|\| ^24.11.0 \|\| >=26.0.0`; ABI 137) |
|
||||
| Host | Windows 11 x64 (build 26200) |
|
||||
|
||||
Exercised live: a clean `setup.mjs --reuse` run reached `setup complete`; an MCP `initialize` handshake
|
||||
@@ -295,6 +307,13 @@ Not exercised end to end here, and therefore not claimed: the ~775 MB JDK + Ghid
|
||||
by `--dry-run`, but this machine adopted its existing installs with `--reuse`), and first runs on
|
||||
macOS/Linux.
|
||||
|
||||
On 2026-10-23 the same chain was re-verified after the host Node moved to **24.22.1**: the vendored
|
||||
`rea-windows-x64.node` and both `conpty` addons loaded under ABI 137; `binary_overview` on `conpty.node`
|
||||
reported 10 segments / 1178 procedures / 618 strings; `procedure_pseudo_code` returned real decompiled
|
||||
output; and `java -version` from the vendored JDK answered `21.0.12.1`. The one thing that did **not**
|
||||
work on the first try was the first `list_segments` — it timed out at 64 s of cold Ghidra analysis, which
|
||||
is what the smoke step and the note above are for.
|
||||
|
||||
## Scope and caveats
|
||||
|
||||
- REA exposes 207 tools at 6.7.0, so every session's tool list grows.
|
||||
@@ -410,7 +429,10 @@ back on the next start.
|
||||
- **Upstream REA / Ghidra**: update the pins in the manifest, then re-run setup. REA version-gates Ghidra
|
||||
(12.1.x today), so update REA first.
|
||||
|
||||
`check-updates.ps1` (Windows PowerShell) reports the state of all of these at once.
|
||||
`check-updates.ps1` (Windows PowerShell) reports the state of all of these at once. `npm run check-updates`
|
||||
forwards to it on Windows (PowerShell 5.1, then `pwsh`); on macOS/Linux it prints why the script cannot
|
||||
answer there and names the two portable commands instead — `node bin/rea-shim.mjs --shim-status` and
|
||||
`node scripts/setup.mjs --dry-run` — exiting non-zero rather than reporting a green it did not earn.
|
||||
|
||||
## Still prefer the configuration-only 1.x line?
|
||||
|
||||
|
||||
+23
-6
@@ -188,6 +188,9 @@ node scripts/setup.mjs --reuse # 或先 --dry-run 看计划
|
||||
- **`!!js` 标量必须加引号。** 裸标量里同时出现 `? ` 和 `: ` 时,它就不是一个标量了:YAML 会把它读成显式键值对,于是 loader 拿到的是一个 **mapping** 而不是表达式节点。用 harness 自己的方言实测(`JSON_SCHEMA` + `app.asar` 里的 `!!js` 类型),不加引号时 `args[0]` 变成 `{"[object Object]": "..."}` —— 一个行根本无法用来启动的对象。加引号消除歧义,内部的 JS 字符串改用单引号。
|
||||
- **必须用正斜杠,不能用反斜杠。** YAML 单引号标量会保留 `\` 字面量,接着 JavaScript 会把它吃掉:`'\plugins'` 不是路径而是 BEL 控制字符,`'\rea-bundle'` 变成回车、`'\bin'` 变成退格 —— 路径被静默摧毁。正斜杠在 Windows 与 POSIX 上都成立。
|
||||
- **`DSH_HOME` 的兜底是 `<home>/.dsh`,不是 `<home>`。** shim、`setup.mjs`、`bootstrap.ps1` 三者都把未设置的 `DSH_HOME` 解析到 `<home>/.dsh/...`;兜底少这一层,行就会指向供给永远不会写的位置 —— 而在 `DSH_HOME` 恰好被设置的机器上,这个错误完全不可见。
|
||||
- **`DSH_HOME`、`USERPROFILE`、`HOME` 三者都为空时,表达式会得到 `/.dsh/...`** —— 这是纯拼接唯一无法兜住
|
||||
的组合,出现在没有主目录的服务账户/容器里。此时那一行起不来,而 `failOnStartupError: false` 会让它保持安静,
|
||||
症状只有「没有 `mcp__rea__*` 工具」。这类账户请显式设置 `DSH_HOME`。
|
||||
- **`!!js` 表达式里没有 `path`/`require`**:`ctx` 是 loader 作用域,所以 DSH_HOME 的兜底写成纯字符串拼接。
|
||||
|
||||
`npm run bundle:audit` 会把前三条钉住:它用「`DSH_HOME` 未设 / 为空 / 已设」三种情形求值这个表达式,并拒绝会被 YAML 读成 mapping 的裸标量。
|
||||
@@ -246,12 +249,18 @@ shim 会在启动时提示冲突 —— `setup.mjs` 会把运行时的 DSH home
|
||||
(`configuration` / `installation` / `headless` / `native_decompiler` / `java`),应当全为 `ok: true`。
|
||||
然后 `open_binary` 一个 PE、`binary_overview`、`procedure_pseudo_code`。
|
||||
|
||||
供给本身结束时也会跑两道探针:`rea doctor --json`(如实报告,并点名未满足项),以及**与 MCP 行完全相同的
|
||||
shim 启动**。
|
||||
供给本身结束时跑三道检查:`rea doctor --json`(如实报告,并点名未满足项)、一次 **Ghidra 冒烟导入**
|
||||
(用 JDK 自带的 `bin/java` 跑 `rea analyze`,导不进就判本次供给失败;`--skip-smoke` 可关),以及
|
||||
**与 MCP 行完全相同的 shim 启动**。之所以要冒烟:`doctor` 只校验路径、版本与会用哪个 Java,**从不导入
|
||||
二进制**,桥接或 native addon 坏了,过去只能在第一次 MCP 调用超时时才撞见。
|
||||
|
||||
**每个新目标的第一次分析都很慢。** 本机实测:小 PE 36 s,6 MB 的 DLL 64 s —— 每次导入都要起 JVM 并跑
|
||||
Ghidra 的默认自动分析。如果大文件的第一次 `open_binary` → `list_segments` 超时,那是这个成本、不是引擎坏了:
|
||||
再调一次,已打开的目标会立刻返回(它保留着自己的临时项目)。
|
||||
|
||||
## 实测环境
|
||||
|
||||
作者机器,2026-10-13 / 14 / 16 / 19:
|
||||
作者机器,2026-10-13 / 14 / 16 / 19 / 23:
|
||||
|
||||
| 组件 | 版本 |
|
||||
|---|---|
|
||||
@@ -259,8 +268,8 @@ shim 启动**。
|
||||
| DeepSeek Harness | desktop runtime **0.2.0-rc.2**,内含 dsh CLI 0.1.7-rc.2 |
|
||||
| `@deepseek-ai/dsh-mcp-client`(行 provider) | 随 harness 打包 |
|
||||
| Ghidra | 12.1.4 |
|
||||
| JDK | Temurin 21(供给)/ Microsoft OpenJDK 25.0.4(复用) |
|
||||
| Node.js | 24.21.0(REA engines:`^22.19.0 \|\| ^24.11.0 \|\| >=26.0.0`) |
|
||||
| JDK | Temurin 21(供给)/ Amazon Corretto 25.0.5(宿主复用) |
|
||||
| Node.js | **24.22.1**(复用;REA engines:`^22.19.0 \|\| ^24.11.0 \|\| >=26.0.0`,ABI 137) |
|
||||
| 宿主 | Windows 11 x64(build 26200) |
|
||||
|
||||
实测跑过:一次干净的 `setup.mjs --reuse` 走到 `setup complete`;一次**不经 PATH 上的 `rea`、完全经由
|
||||
@@ -271,6 +280,11 @@ shim** 的 MCP `initialize` 握手返回 `serverInfo {name: rea, version: 6.7.0}
|
||||
本机**没有**端到端跑过、因此不做承诺的:约 775 MB 的 JDK + Ghidra 下载分支(`--dry-run` 有计划,本机用
|
||||
`--reuse` 复用了已有安装),以及 macOS/Linux 的首次运行。
|
||||
|
||||
2026-10-23 宿主 Node 换到 **24.22.1** 后,同一条链路重验过:vendor 里的 `rea-windows-x64.node` 与两个
|
||||
`conpty` addon 在 ABI 137 下加载成功;`conpty.node` 的 `binary_overview` 报出 10 个段 / 1178 个函数 /
|
||||
618 个字符串;`procedure_pseudo_code` 返回了真实反编译结果;vendor 里的 JDK `java -version` 回 `21.0.12.1`。
|
||||
唯一**第一次没成**的是 `list_segments`:它撞上 Ghidra 冷分析的 64 s 而超时 —— 这正是上面冒烟步与那条说明的由来。
|
||||
|
||||
## 边界与注意事项
|
||||
|
||||
- REA 在 6.7.0 有 207 个工具,每个会话的工具列表都会变长。
|
||||
@@ -373,7 +387,10 @@ node scripts/setup.mjs # 或:powershell -File scripts\rea-bundle-boot
|
||||
- **上游 REA / Ghidra**:改清单里的钉子再重跑 setup。REA 对 Ghidra 有版本闸门(目前 12.1.x),
|
||||
所以先更新 REA。
|
||||
|
||||
`check-updates.ps1`(Windows PowerShell)会一次性报告以上各项的状态。
|
||||
`check-updates.ps1`(Windows PowerShell)会一次性报告以上各项的状态。`npm run check-updates` 在 Windows
|
||||
上转发给它(优先 PowerShell 5.1,其次 `pwsh`);在 macOS/Linux 上它会说明这个脚本为何在那里给不出答案,
|
||||
并列出两条可移植命令 —— `node bin/rea-shim.mjs --shim-status` 与 `node scripts/setup.mjs --dry-run` ——
|
||||
且以非零码退出,不会报一个自己没查出来的「全绿」。
|
||||
|
||||
## 还想用纯配置的 1.x 线?
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"jdk": "21",
|
||||
"apktool": "3.0.3",
|
||||
"jadxHeadlessMcp": "0.7.1",
|
||||
"node": "24.21.0",
|
||||
"node": "24.22.1",
|
||||
"nodeEngines": "^22.19.0 || ^24.11.0 || >=26.0.0"
|
||||
},
|
||||
"components": {
|
||||
@@ -42,60 +42,60 @@
|
||||
},
|
||||
"sources": {
|
||||
"win32-x64": {
|
||||
"version": "24.21.0",
|
||||
"url": "https://nodejs.org/dist/v24.21.0/node-v24.21.0-win-x64.zip",
|
||||
"version": "24.22.1",
|
||||
"url": "https://nodejs.org/dist/v24.22.1/node-v24.22.1-win-x64.zip",
|
||||
"format": "zip",
|
||||
"size": 37618919,
|
||||
"sha256": "158f7685b44de51f6c0df1d153526cbcd3e1bc739a8dfc607721cef75de9e541",
|
||||
"sha256Source": "https://nodejs.org/dist/v24.21.0/SHASUMS256.txt (line: node-v24.21.0-win-x64.zip)",
|
||||
"sha256VerifiedOn": "2026-10-13",
|
||||
"note": "Checksum and unpack verified on this machine: the extracted node.exe runs standalone (v24.21.0), no installer and no registry writes."
|
||||
"size": 37748064,
|
||||
"sha256": "d2046d28c2c3dcd211ac5145c0bf30e2047ef9dfb306693c8c6e2eccd8b95fa5",
|
||||
"sha256Source": "https://nodejs.org/dist/v24.22.1/SHASUMS256.txt (line: node-v24.22.1-win-x64.zip)",
|
||||
"sha256VerifiedOn": "2026-10-23",
|
||||
"note": "Checksum and unpack verified on this machine: the extracted node.exe runs standalone (v24.22.1), no installer and no registry writes. 24.22.1 is Node's 2026-10-21 security release (Brotli decoder out-of-bounds write RCE, two TLS hostname bypasses, eMudhra intermediate removal); 24.21.0 must not be redistributed."
|
||||
},
|
||||
"win32-arm64": {
|
||||
"version": "24.21.0",
|
||||
"url": "https://nodejs.org/dist/v24.21.0/node-v24.21.0-win-arm64.zip",
|
||||
"version": "24.22.1",
|
||||
"url": "https://nodejs.org/dist/v24.22.1/node-v24.22.1-win-arm64.zip",
|
||||
"format": "zip",
|
||||
"size": 33679608,
|
||||
"sha256": "8779b1bde1d39f8d420e3b57aa657b39891af434d3de44a919044cec06785921",
|
||||
"sha256Source": "https://nodejs.org/dist/v24.21.0/SHASUMS256.txt (line: node-v24.21.0-win-arm64.zip), size confirmed by HEAD",
|
||||
"sha256VerifiedOn": "2026-10-13"
|
||||
"size": 33786263,
|
||||
"sha256": "a0c807319fede24c4a0d8f84564c82a619bd313b16b10c007c383c2609e99171",
|
||||
"sha256Source": "https://nodejs.org/dist/v24.22.1/SHASUMS256.txt (line: node-v24.22.1-win-arm64.zip), size confirmed by HEAD",
|
||||
"sha256VerifiedOn": "2026-10-23"
|
||||
},
|
||||
"darwin-x64": {
|
||||
"version": "24.21.0",
|
||||
"url": "https://nodejs.org/dist/v24.21.0/node-v24.21.0-darwin-x64.tar.gz",
|
||||
"version": "24.22.1",
|
||||
"url": "https://nodejs.org/dist/v24.22.1/node-v24.22.1-darwin-x64.tar.gz",
|
||||
"format": "tar.gz",
|
||||
"size": 54203979,
|
||||
"sha256": "1462cb3b3046b815cf8ea436d3da450ec1a9f11dac7e5a46b0ada5305d7e8097",
|
||||
"sha256Source": "https://nodejs.org/dist/v24.21.0/SHASUMS256.txt (line: node-v24.21.0-darwin-x64.tar.gz)",
|
||||
"sha256VerifiedOn": "2026-10-13"
|
||||
"size": 54382566,
|
||||
"sha256": "d132557e7ee2b85af08b0c8c34338e65c6430f2238d01f727728ef42dc83b035",
|
||||
"sha256Source": "https://nodejs.org/dist/v24.22.1/SHASUMS256.txt (line: node-v24.22.1-darwin-x64.tar.gz)",
|
||||
"sha256VerifiedOn": "2026-10-23"
|
||||
},
|
||||
"darwin-arm64": {
|
||||
"version": "24.21.0",
|
||||
"url": "https://nodejs.org/dist/v24.21.0/node-v24.21.0-darwin-arm64.tar.gz",
|
||||
"version": "24.22.1",
|
||||
"url": "https://nodejs.org/dist/v24.22.1/node-v24.22.1-darwin-arm64.tar.gz",
|
||||
"format": "tar.gz",
|
||||
"size": 52909993,
|
||||
"sha256": "bed7eea5325e1108f32ce5228ddd6a5f0f08a499ee42aa7442aea583702f6057",
|
||||
"sha256Source": "https://nodejs.org/dist/v24.21.0/SHASUMS256.txt (line: node-v24.21.0-darwin-arm64.tar.gz)",
|
||||
"sha256VerifiedOn": "2026-10-13"
|
||||
"size": 53062778,
|
||||
"sha256": "89c682593d76cd44233fa53b1563264e7de9cbb189cad169135c5b4fadfbc0a7",
|
||||
"sha256Source": "https://nodejs.org/dist/v24.22.1/SHASUMS256.txt (line: node-v24.22.1-darwin-arm64.tar.gz)",
|
||||
"sha256VerifiedOn": "2026-10-23"
|
||||
},
|
||||
"linux-x64": {
|
||||
"version": "24.21.0",
|
||||
"url": "https://nodejs.org/dist/v24.21.0/node-v24.21.0-linux-x64.tar.xz",
|
||||
"version": "24.22.1",
|
||||
"url": "https://nodejs.org/dist/v24.22.1/node-v24.22.1-linux-x64.tar.xz",
|
||||
"format": "tar.xz",
|
||||
"size": 31890184,
|
||||
"sha256": "fd8e59d5a511510f6a298afb548f18c7d2b1be404d8b4a27d94fbe49f56cb2d6",
|
||||
"sha256Source": "https://nodejs.org/dist/v24.21.0/SHASUMS256.txt (line: node-v24.21.0-linux-x64.tar.xz)",
|
||||
"sha256VerifiedOn": "2026-10-13",
|
||||
"size": 32026916,
|
||||
"sha256": "6c172a9ab6718f7b801fb5a7bd486af2ee5e32758086a04b84fcce2aa77095e6",
|
||||
"sha256Source": "https://nodejs.org/dist/v24.22.1/SHASUMS256.txt (line: node-v24.22.1-linux-x64.tar.xz)",
|
||||
"sha256VerifiedOn": "2026-10-23",
|
||||
"note": ".tar.xz is not unpackable by Windows Expand-Archive; bsdtar handles it. A .tar.gz twin exists for every platform if xz support is ever in doubt."
|
||||
},
|
||||
"linux-arm64": {
|
||||
"version": "24.21.0",
|
||||
"url": "https://nodejs.org/dist/v24.21.0/node-v24.21.0-linux-arm64.tar.xz",
|
||||
"version": "24.22.1",
|
||||
"url": "https://nodejs.org/dist/v24.22.1/node-v24.22.1-linux-arm64.tar.xz",
|
||||
"format": "tar.xz",
|
||||
"size": 30859684,
|
||||
"sha256": "6ad1325edbdb5649c379b75a237147a666c95d4f9ae8d340fef2d1575d289ad2",
|
||||
"sha256Source": "https://nodejs.org/dist/v24.21.0/SHASUMS256.txt (line: node-v24.21.0-linux-arm64.tar.xz)",
|
||||
"sha256VerifiedOn": "2026-10-13"
|
||||
"size": 30978868,
|
||||
"sha256": "dbabf7a1cbf947e6059ffad2c27b80041db97dea1e553b62afdee7f13fca336e",
|
||||
"sha256Source": "https://nodejs.org/dist/v24.22.1/SHASUMS256.txt (line: node-v24.22.1-linux-arm64.tar.xz)",
|
||||
"sha256VerifiedOn": "2026-10-23"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -114,7 +114,7 @@
|
||||
"size": 205073461,
|
||||
"sha256": "f9d6e191ab098c0d416e7d588a24420a8621cd2f4720dab2459b8b7b2d2d8b4e",
|
||||
"sha256Source": "Adoptium API /v3/assets/latest/21/hotspot (binary.package.checksum), cross-checked against the per-asset .zip.sha256.txt sidecar",
|
||||
"sha256VerifiedOn": "2026-10-13",
|
||||
"sha256VerifiedOn": "2026-10-23",
|
||||
"metaApi": "https://api.adoptium.net/v3/assets/latest/21/hotspot?architecture=x64&image_type=jdk&os=windows&vendor=eclipse",
|
||||
"note": "Unpacked and exercised on this machine: bin/javac.exe and jmods are present, java --list-modules reports jdk.compiler, and REA's Ghidra provider accepted it as a 64-bit full JDK 21+."
|
||||
},
|
||||
@@ -125,7 +125,7 @@
|
||||
"size": 194316575,
|
||||
"sha256": "44db0f08196daf19a47f90d13388b0c943b67663cb537f998fe29e836fa842ce",
|
||||
"sha256Source": "Adoptium API (binary.package.checksum)",
|
||||
"sha256VerifiedOn": "2026-10-13",
|
||||
"sha256VerifiedOn": "2026-10-23",
|
||||
"metaApi": "https://api.adoptium.net/v3/assets/latest/21/hotspot?architecture=x64&image_type=jdk&os=mac&vendor=eclipse"
|
||||
},
|
||||
"darwin-arm64": {
|
||||
@@ -135,7 +135,7 @@
|
||||
"size": 200073404,
|
||||
"sha256": "3623232f33a9c3baadf304480b2535f9a3cba8a58d42ecbb438ba267315d9998",
|
||||
"sha256Source": "Adoptium API (binary.package.checksum)",
|
||||
"sha256VerifiedOn": "2026-10-13",
|
||||
"sha256VerifiedOn": "2026-10-23",
|
||||
"metaApi": "https://api.adoptium.net/v3/assets/latest/21/hotspot?architecture=aarch64&image_type=jdk&os=mac&vendor=eclipse"
|
||||
},
|
||||
"linux-x64": {
|
||||
@@ -145,7 +145,7 @@
|
||||
"size": 207473347,
|
||||
"sha256": "ce79869e1307ed8ee1e2baa86a412b1eb5b75d10a01006d788a6f968bcfaee94",
|
||||
"sha256Source": "Adoptium API (binary.package.checksum)",
|
||||
"sha256VerifiedOn": "2026-10-13",
|
||||
"sha256VerifiedOn": "2026-10-23",
|
||||
"metaApi": "https://api.adoptium.net/v3/assets/latest/21/hotspot?architecture=x64&image_type=jdk&os=linux&vendor=eclipse"
|
||||
},
|
||||
"linux-arm64": {
|
||||
@@ -155,7 +155,7 @@
|
||||
"size": 205641175,
|
||||
"sha256": "23e37e026f12f3e706f18938ff611db3032d075b09d0879a25d06718c773e223",
|
||||
"sha256Source": "Adoptium API (binary.package.checksum)",
|
||||
"sha256VerifiedOn": "2026-10-13",
|
||||
"sha256VerifiedOn": "2026-10-23",
|
||||
"metaApi": "https://api.adoptium.net/v3/assets/latest/21/hotspot?architecture=aarch64&image_type=jdk&os=linux&vendor=eclipse"
|
||||
}
|
||||
}
|
||||
@@ -176,7 +176,7 @@
|
||||
"sha256": "ddac49f903da9d5bac833e5cc79395098b9c33cfd3279be5f31bd00387d2d4db",
|
||||
"sha256Source": "GitHub release body of the pinned tag: api.github.com/repos/NationalSecurityAgency/ghidra/releases/tags/Ghidra_12.1.4_build -> .body, line '* SHA-256: `ddac49f9...`'. No checksum asset exists (six candidate names 404). Always read the pinned tag, never releases/latest, because the body changes.",
|
||||
"releaseApi": "https://api.github.com/repos/NationalSecurityAgency/ghidra/releases/tags/Ghidra_12.1.4_build",
|
||||
"sha256VerifiedOn": "2026-10-13",
|
||||
"sha256VerifiedOn": "2026-10-23",
|
||||
"note": "No platform split for the public release: one zip serves Windows/macOS/Linux. Unpacked and sniffed here (support/analyzeHeadless.bat and the win_x86_64 native decompiler are present, and REA reported provider version 12.1.4). Windows traps: a path containing '!' or '^' breaks Ghidra, and macOS native components are not shipped (they must be built with support/gradle/gradlew buildNatives). Ghidra requires JDK 21+; application.java.max is empty at this tag, so a newer JDK is permitted."
|
||||
}
|
||||
}
|
||||
@@ -196,7 +196,7 @@
|
||||
"size": 15478013,
|
||||
"sha256": "dbf930b076c6b9be08d57c449cacefc3bdd6b71ebd59b3066fc0e1f5b14f9423",
|
||||
"sha256Source": "Vendor-published in the 3.0.3 release post (apktool.org/blog/apktool-3.0.3/), md5 50d98c920b005d2fcdc18d3d52e3f23b. Independently reproduced here: this is the hash of the 15,478,013-byte apktool_3.0.3.jar.",
|
||||
"sha256VerifiedOn": "2026-10-13",
|
||||
"sha256VerifiedOn": "2026-10-23",
|
||||
"moveTo": "apktool",
|
||||
"saveAs": "apktool.jar"
|
||||
}
|
||||
@@ -301,7 +301,7 @@
|
||||
"size": 42619858,
|
||||
"sha256": "6e5eacf500b64292bfb73c49797c1958f6ee44646e43e868039ae7feb573ff75",
|
||||
"sha256Source": "Vendor-published sidecar asset releases/download/v0.7.1/jadx-headless-mcp.sha256 (98 bytes, one line); the value was independently reproduced here by hashing the downloaded 42,619,858-byte jar. 0.7.1 is the newest release and REA requires exactly this bridge build: the project's own build.gradle.kts documents that the fat jar must be shaded with duplicatesStrategy INCLUDE, otherwise only KotlinMetadataPlugin registers and every APK resolves to a single R class. Versions 0.6.0 and earlier predate that fix.",
|
||||
"sha256VerifiedOn": "2026-10-13",
|
||||
"sha256VerifiedOn": "2026-10-23",
|
||||
"moveTo": "jadx",
|
||||
"saveAs": "jadx-headless-mcp-0.7.1-all.jar"
|
||||
}
|
||||
@@ -323,8 +323,8 @@
|
||||
"format": "zip",
|
||||
"size": 8044989,
|
||||
"sha256": "45f4d63113e895ebde0c90f194099a4676b6ac653bd28d54314a9e022bbc1a99",
|
||||
"sha256Source": "Google publishes no checksum for this rolling asset; measured on 2026-10-13 and the archive verified readable (15 entries incl. adb.exe). The archive's own platform-tools/source.properties reports Pkg.Revision=37.0.1, so the real pin is 'platform-tools 37.0.1'. Google's repository2-3.xml carries size+checksum for versioned archives if a vendor hash is ever required.",
|
||||
"sha256VerifiedOn": "2026-10-13",
|
||||
"sha256Source": "Google publishes no checksum for this rolling asset; measured on 2026-10-23 and the archive verified readable (15 entries incl. adb.exe). The archive's own platform-tools/source.properties reports Pkg.Revision=37.0.1, so the real pin is 'platform-tools 37.0.1'. Google's repository2-3.xml carries size+checksum for versioned archives if a vendor hash is ever required.",
|
||||
"sha256VerifiedOn": "2026-10-23",
|
||||
"note": "Rolling 'latest' URL pinned by observation; setup re-verifies against this value and reports drift. Android SDK components are not OSI-licensed and Google restricts redistribution, so this is fetched from Google at install time and never re-hosted. Also contains fastboot, sqlite3, mke2fs and the USB API DLLs."
|
||||
},
|
||||
"darwin-x64": {
|
||||
@@ -333,8 +333,8 @@
|
||||
"format": "zip",
|
||||
"size": 16110554,
|
||||
"sha256": "ee39ad5967e95c2a07f04dbcbde96b1a0c916ba376096db5d2f498b7727a5d1d",
|
||||
"sha256Source": "Google publishes no checksum for this rolling asset; measured on 2026-10-13 by downloading the archive and hashing it locally (16,110,554 bytes)",
|
||||
"sha256VerifiedOn": "2026-10-13",
|
||||
"sha256Source": "Google publishes no checksum for this rolling asset; measured on 2026-10-23 by downloading the archive and hashing it locally (16,110,554 bytes)",
|
||||
"sha256VerifiedOn": "2026-10-23",
|
||||
"note": "Rolling 'latest' URL pinned by observation; setup re-verifies against this value and reports drift."
|
||||
},
|
||||
"darwin-arm64": {
|
||||
@@ -343,8 +343,8 @@
|
||||
"format": "zip",
|
||||
"size": 16110554,
|
||||
"sha256": "ee39ad5967e95c2a07f04dbcbde96b1a0c916ba376096db5d2f498b7727a5d1d",
|
||||
"sha256Source": "Same darwin archive as x64 (measured 2026-10-13); Google publishes one darwin zip for both architectures.",
|
||||
"sha256VerifiedOn": "2026-10-13",
|
||||
"sha256Source": "Same darwin archive as x64 (measured 2026-10-23); Google publishes one darwin zip for both architectures.",
|
||||
"sha256VerifiedOn": "2026-10-23",
|
||||
"note": "The darwin archive contains x86_64 binaries. On Apple silicon they run only through Rosetta 2; without it adb cannot start and this optional component is reported unavailable."
|
||||
},
|
||||
"linux-x64": {
|
||||
@@ -353,8 +353,8 @@
|
||||
"format": "zip",
|
||||
"size": 9054187,
|
||||
"sha256": "d230f13842f60f782a8645f9c813f8f845bf36089ea7289f28c48f17979313f1",
|
||||
"sha256Source": "Google publishes no checksum for this rolling asset; measured on 2026-10-13 by downloading the archive and hashing it locally (9,054,187 bytes)",
|
||||
"sha256VerifiedOn": "2026-10-13",
|
||||
"sha256Source": "Google publishes no checksum for this rolling asset; measured on 2026-10-23 by downloading the archive and hashing it locally (9,054,187 bytes)",
|
||||
"sha256VerifiedOn": "2026-10-23",
|
||||
"note": "The linux archive contains x86_64 binaries. Google publishes no arm64 platform-tools and this bundle does not emulate one, so on linux-arm64 the entry below is offered but its probe is expected to fail — reported as an optional component being unavailable, never as a broken install."
|
||||
},
|
||||
"linux-arm64": {
|
||||
@@ -363,8 +363,8 @@
|
||||
"format": "zip",
|
||||
"size": 9054187,
|
||||
"sha256": "d230f13842f60f782a8645f9c813f8f845bf36089ea7289f28c48f17979313f1",
|
||||
"sha256Source": "Google publishes no arm64 platform-tools; this is the same x86_64 linux archive as x64 (measured 2026-10-13), recorded so the mismatch is explicit instead of implied.",
|
||||
"sha256VerifiedOn": "2026-10-13",
|
||||
"sha256Source": "Google publishes no arm64 platform-tools; this is the same x86_64 linux archive as x64 (measured 2026-10-23), recorded so the mismatch is explicit instead of implied.",
|
||||
"sha256VerifiedOn": "2026-10-23",
|
||||
"note": "Kept so an arm64 host reports a precise failure instead of \"no source\". adb here is x86_64: it runs under qemu/binfmt where the host provides it, and otherwise the probe fails and the report names it."
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"check-updates": "powershell -NoProfile -File ./check-updates.ps1",
|
||||
"check-updates": "node ./scripts/check-updates.mjs",
|
||||
"bundle:setup": "node ./scripts/setup.mjs",
|
||||
"bundle:status": "node ./bin/rea-shim.mjs --shim-status",
|
||||
"bundle:dry-run": "node ./scripts/setup.mjs --reuse --dry-run",
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* `npm run check-updates` — a portable entry for a Windows-only script.
|
||||
*
|
||||
* `check-updates.ps1` is a Windows PowerShell tool on purpose: it reads
|
||||
* `%APPDATA%\npm`, prefers a `rea.cmd` shim over `rea.ps1`, and walks the
|
||||
* profile the way the desktop app lays it out. On macOS/Linux even a working
|
||||
* `pwsh` would run those branches against paths that do not exist there, so
|
||||
* "make the .ps1 cross-platform" is not the fix — the honest answer is to say
|
||||
* so, and to name the two commands that DO work everywhere.
|
||||
*
|
||||
* This wrapper therefore:
|
||||
* - on Windows: runs `powershell` (the 5.1 the script is written for), and
|
||||
* falls back to `pwsh` when 5.1 is absent;
|
||||
* - elsewhere: prints why, plus the portable equivalents, and exits non-zero
|
||||
* so a CI step cannot mistake "not applicable" for "all green".
|
||||
*
|
||||
* Its pure parts are exported so a unit test can pin both branches without
|
||||
* spawning anything.
|
||||
*/
|
||||
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
/** Repository root: this file lives in <root>/scripts/. */
|
||||
export const ROOT = fileURLToPath(new URL('..', import.meta.url))
|
||||
|
||||
/**
|
||||
* PowerShell executables to try, in order, for a platform.
|
||||
*
|
||||
* Empty on POSIX: the script's own probes are Windows-specific, so running it
|
||||
* there would produce a confident wrong answer instead of a clear "not here".
|
||||
*/
|
||||
export function powerShellCandidates(platform = process.platform) {
|
||||
return platform === 'win32' ? ['powershell', 'pwsh'] : []
|
||||
}
|
||||
|
||||
/** The message used when this platform (or this machine) cannot run the script. */
|
||||
export function unsupportedMessage(platform = process.platform) {
|
||||
const where = platform === 'win32'
|
||||
? 'No PowerShell (powershell or pwsh) was found on this machine.'
|
||||
: `check-updates.ps1 is a Windows PowerShell tool; this host is ${platform}.`
|
||||
return [
|
||||
where,
|
||||
'',
|
||||
'The same questions, answered with commands that work on every platform:',
|
||||
' node bin/rea-shim.mjs --shim-status # provisioned bundle, launch choice, versions',
|
||||
' node scripts/setup.mjs --dry-run # what a setup run would still fetch',
|
||||
'',
|
||||
'On Windows, `npm run check-updates` also compares the installed versions',
|
||||
'against the upstream registries and the profile registration.',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function main(argv) {
|
||||
const script = join(ROOT, 'check-updates.ps1')
|
||||
if (!existsSync(script)) {
|
||||
process.stderr.write(`check-updates: ${script} is missing from this package.\n`)
|
||||
return 1
|
||||
}
|
||||
|
||||
const candidates = powerShellCandidates()
|
||||
if (candidates.length === 0) {
|
||||
process.stderr.write(`${unsupportedMessage()}\n`)
|
||||
return 1
|
||||
}
|
||||
|
||||
const env = process.env
|
||||
const attempts = []
|
||||
for (const command of candidates) {
|
||||
const result = spawnSync(command, ['-NoProfile', '-File', script, ...argv], {
|
||||
stdio: 'inherit',
|
||||
cwd: ROOT,
|
||||
env,
|
||||
windowsHide: true,
|
||||
})
|
||||
if (result.error && result.error.code === 'ENOENT') {
|
||||
attempts.push(command)
|
||||
continue
|
||||
}
|
||||
if (result.error) {
|
||||
process.stderr.write(`check-updates: ${command} failed to start: ${result.error.message}\n`)
|
||||
return 1
|
||||
}
|
||||
return typeof result.status === 'number' ? result.status : 1
|
||||
}
|
||||
|
||||
process.stderr.write(
|
||||
`check-updates: tried ${attempts.join(', ')} — none is on PATH.\n\n${unsupportedMessage()}\n`,
|
||||
)
|
||||
return 1
|
||||
}
|
||||
|
||||
if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) {
|
||||
process.exitCode = main(process.argv.slice(2))
|
||||
}
|
||||
+114
-6
@@ -94,6 +94,7 @@ function parseArgs(argv) {
|
||||
json: false,
|
||||
only: null,
|
||||
offline: false,
|
||||
skipSmoke: false,
|
||||
// Two different clocks, because one number cannot serve both: a route must
|
||||
// be declared dead quickly when the socket never opens, but a slow-but-alive
|
||||
// transfer must not be killed for taking long overall (measured here:
|
||||
@@ -110,6 +111,7 @@ function parseArgs(argv) {
|
||||
else if (arg === "--dry-run") options.dryRun = true;
|
||||
else if (arg === "--json") options.json = true;
|
||||
else if (arg === "--offline") options.offline = true;
|
||||
else if (arg === "--skip-smoke") options.skipSmoke = true;
|
||||
else if (arg === "--yes") continue;
|
||||
else if (arg === "--only") options.only = (argv[++i] ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
||||
else if (arg === "--help" || arg === "-h") options.help = true;
|
||||
@@ -889,7 +891,44 @@ function reusableNodeChecked(candidate) {
|
||||
if (typeof regex === "string" && regex !== "" && !new RegExp(regex, "u").test(stdout)) {
|
||||
return { ok: false, detail: `it reports "${firstLine}", which REA's engines reject` };
|
||||
}
|
||||
return { ok: true };
|
||||
// Report the version the candidate actually printed, not the interpreter that
|
||||
// happens to be running setup. They differ whenever the adopted Node is not
|
||||
// the one executing this script — `REA_BUNDLE_NODE`, or a PATH whose first hit
|
||||
// is a different install — and recording the running interpreter silently
|
||||
// wrote a bundle.json that disagreed with the runtime the row launches.
|
||||
return { ok: true, version: firstLine.replace(/^v/, "").trim() || null };
|
||||
}
|
||||
|
||||
/**
|
||||
* The version a reused JDK or Ghidra reports about itself.
|
||||
*
|
||||
* Both reuse branches used to return only `{path, how}`, so `bundle.json` fell
|
||||
* back to `merged.<name>?.version ?? null` and recorded `""` for a JDK or Ghidra
|
||||
* the machine had adopted — a record that names a runtime but cannot say which
|
||||
* one. `check-updates.ps1` compares against exactly that field, so the drift it
|
||||
* exists to report became invisible for precisely the components a reuse run
|
||||
* touches. Both files are the vendor's own declaration, not a guess:
|
||||
* `release` carries `JAVA_VERSION="…"`, `Ghidra/application.properties` carries
|
||||
* `application.version=…`.
|
||||
*/
|
||||
function readJdkVersion(root) {
|
||||
try {
|
||||
const release = readFileSync(path.join(root, "release"), "utf8");
|
||||
const match = /^JAVA_VERSION="?([^"\r\n]+)"?/m.exec(release);
|
||||
return match ? match[1].trim() : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function readGhidraVersion(root) {
|
||||
try {
|
||||
const properties = readFileSync(path.join(root, "Ghidra", "application.properties"), "utf8");
|
||||
const match = /^application\.version=(.+)$/m.exec(properties);
|
||||
return match ? match[1].trim() : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Adopt an existing installation instead of downloading a duplicate. */
|
||||
@@ -912,7 +951,7 @@ function reuseExisting(name, root) {
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
const checked = reusableNodeChecked(candidate.path);
|
||||
if (checked.ok) return candidate;
|
||||
if (checked.ok) return { ...candidate, version: checked.version ?? null };
|
||||
step("node", "plan", `not reusing ${candidate.how} node ${candidate.path}: ${checked.detail}`);
|
||||
}
|
||||
return null;
|
||||
@@ -920,14 +959,16 @@ function reuseExisting(name, root) {
|
||||
if (name === "jdk") {
|
||||
for (const [variable, how] of [["JAVA_HOME", "JAVA_HOME"]]) {
|
||||
const home = process.env[variable];
|
||||
if (home && existsSync(path.join(home, "bin", `javac${EXE}`))) return { path: home, how };
|
||||
if (home && existsSync(path.join(home, "bin", `javac${EXE}`))) {
|
||||
return { path: path.normalize(home), how, version: readJdkVersion(home) };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (name === "ghidra") {
|
||||
const install = process.env.GHIDRA_INSTALL_DIR;
|
||||
if (install && existsSync(path.join(install, "support", IS_WINDOWS ? "analyzeHeadless.bat" : "analyzeHeadless"))) {
|
||||
return { path: install, how: "GHIDRA_INSTALL_DIR" };
|
||||
return { path: path.normalize(install), how: "GHIDRA_INSTALL_DIR", version: readGhidraVersion(install) };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1068,7 +1109,11 @@ async function provisionComponent(name, component, context) {
|
||||
}
|
||||
if (probe.ok) {
|
||||
step(name, "skip", `reusing existing ${name} from ${reused.how}: ${reused.path}`);
|
||||
return { path: reused.path, reused: true };
|
||||
// Carry the adopted installation's own version. Dropping it here made
|
||||
// `bundle.json` record the manifest's pin instead of what was adopted — a
|
||||
// record that names a path but describes a different build, which is the
|
||||
// one thing `check-updates.ps1` exists to catch.
|
||||
return { ...reused, reused: true };
|
||||
}
|
||||
step(name, "plan", `not reusing ${reused.how} ${name} at ${reused.path}: ${probe.detail}`);
|
||||
}
|
||||
@@ -1550,8 +1595,18 @@ if (!options.dryRun) {
|
||||
// what matters is that the CLI runs, emits parseable JSON, and reports the
|
||||
// tool catalog. Unmet checks are reported, not treated as errors.
|
||||
const reaEntry = paths.reaEntry;
|
||||
// The same two variables the mirrored shim exports. Without them REA falls
|
||||
// back to probing the machine, so `doctor` here reported whichever Ghidra and
|
||||
// JDK happened to be installed globally — on a machine with an older global
|
||||
// Ghidra this run's own engine was never the thing being checked, and a broken
|
||||
// vendor copy could still come back healthy.
|
||||
const reaEnv = {
|
||||
...process.env,
|
||||
...(paths.ghidra ? { GHIDRA_INSTALL_DIR: paths.ghidra } : {}),
|
||||
...(paths.jdk ? { JAVA_HOME: paths.jdk } : {}),
|
||||
};
|
||||
if (existsSync(reaEntry)) {
|
||||
const doctor = spawnSync(nodePath, [reaEntry, "doctor", "--json"], { encoding: "utf8", timeout: 300000 });
|
||||
const doctor = spawnSync(nodePath, [reaEntry, "doctor", "--json"], { encoding: "utf8", timeout: 300000, env: reaEnv });
|
||||
const stdout = doctor.stdout ?? "";
|
||||
let parsed = null;
|
||||
try {
|
||||
@@ -1575,6 +1630,59 @@ if (!options.dryRun) {
|
||||
step("probe", "FAIL", "vendored REA CLI is missing; the MCP row will report unprovisioned");
|
||||
}
|
||||
|
||||
// Ghidra smoke: prove the engine runs, not merely that its files exist.
|
||||
//
|
||||
// `doctor` validates paths, versions and which Java would be chosen; it never
|
||||
// imports a binary. The first real import pays a one-time cost (measured here:
|
||||
// ~35 s for a small PE, ~64 s for a 6 MB DLL) for the JVM start plus default
|
||||
// auto-analysis. On a broken bridge or a mismatched native addon the operator
|
||||
// met that only as an MCP tool timeout on the first real analysis — which reads
|
||||
// exactly like "the runtime upgrade broke REA". Importing one small file here
|
||||
// ends provisioning with a measured verdict instead of an inference, and makes
|
||||
// the failure visible while the operator is still in the setup output.
|
||||
//
|
||||
// The input is the JDK's own launcher: it exists on every platform this bundle
|
||||
// provisions, it is small enough to keep the step short, and it is never the
|
||||
// user's file.
|
||||
if (options.skipSmoke) {
|
||||
step("smoke", "skip", "disabled by --skip-smoke");
|
||||
} else if (!existsSync(reaEntry)) {
|
||||
step("smoke", "skip", "no vendored REA CLI to drive");
|
||||
} else if (!paths.ghidra || !paths.jdk) {
|
||||
step("smoke", "skip", "this run resolved no ghidra + jdk pair");
|
||||
} else {
|
||||
const smokeInput = path.join(paths.jdk, "bin", `java${EXE}`);
|
||||
if (!existsSync(smokeInput)) {
|
||||
step("smoke", "skip", `no small input at ${smokeInput}`);
|
||||
} else {
|
||||
const startedAt = Date.now();
|
||||
const smoke = spawnSync(nodePath, [reaEntry, "analyze", smokeInput, "--json"], {
|
||||
encoding: "utf8",
|
||||
timeout: 600000,
|
||||
env: reaEnv,
|
||||
});
|
||||
const elapsed = Math.round((Date.now() - startedAt) / 1000);
|
||||
let evidence = null;
|
||||
try {
|
||||
const text = String(smoke.stdout ?? "");
|
||||
const open = text.indexOf("{");
|
||||
const close = text.lastIndexOf("}");
|
||||
evidence = open >= 0 && close > open ? JSON.parse(text.slice(open, close + 1)) : null;
|
||||
} catch {
|
||||
evidence = null;
|
||||
}
|
||||
if (smoke.status === 0 && evidence && typeof evidence.evidence_id === "string") {
|
||||
step("smoke", "ok", `imported ${path.basename(smokeInput)} in ${elapsed}s via ghidra`);
|
||||
} else {
|
||||
step(
|
||||
"smoke",
|
||||
"FAIL",
|
||||
`ghidra import failed (exit ${smoke.status}, ${elapsed}s): ${`${smoke.stdout ?? ""}${smoke.stderr ?? ""}`.trim().slice(0, 300)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The real end-to-end check: launch the exact shim the MCP row will use
|
||||
// (the mirrored copy, not the source tree) and confirm it relays the CLI.
|
||||
const mirroredShim = path.join(options.root, "bin", "rea-shim.mjs");
|
||||
|
||||
@@ -21,6 +21,7 @@ import test from "node:test";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { HOST_RELEASES, OUTSIDE_HOSTS, checkHostCoverage, declaredHostRange, compareVersions, satisfiesDeclaredRange } from "./host-compat.mjs";
|
||||
import { computeGuideState, reaPinMatches } from "../bin/rea-guide.mjs";
|
||||
import { powerShellCandidates, unsupportedMessage } from "./check-updates.mjs";
|
||||
|
||||
/* ------------------------------------------------- probe token substitution */
|
||||
|
||||
@@ -434,6 +435,81 @@ test("an unavailable optional component is reported, not fatal", () => {
|
||||
}
|
||||
});
|
||||
|
||||
/* ------------------------------ the Ghidra smoke is measured, and it can be off */
|
||||
|
||||
// `doctor` validates paths, versions and which Java would be chosen; it never
|
||||
// imports a binary. The first real import pays a one-time ~35 s for the JVM start
|
||||
// plus default auto-analysis, and when the bridge or the native addon is broken
|
||||
// the operator met that only as an MCP tool timeout on the first real analysis —
|
||||
// which reads exactly like "the runtime upgrade broke REA". The smoke step turns
|
||||
// provisioning into a measured verdict. These tests keep it bounded in both
|
||||
// directions: it must not run when this run resolved no engine, --skip-smoke must
|
||||
// win, and an "ok" that only checked that files exist is the bug it replaced.
|
||||
function runSmokeSetup(root, extra = []) {
|
||||
const stub = path.join(root, "vendor", "node_modules", "rea-agents", "scripts");
|
||||
mkdirSync(stub, { recursive: true });
|
||||
writeFileSync(
|
||||
path.join(stub, "rea.mjs"),
|
||||
'if (process.argv.includes("--version")) { console.log("6.5.0"); } else { console.log(JSON.stringify({ checks: [], identity: {} })); }\n',
|
||||
);
|
||||
return spawnSync(process.execPath, [SETUP_SOURCE, "--reuse", "--only", "node", "--root", root, ...extra], {
|
||||
encoding: "utf8",
|
||||
timeout: 180000,
|
||||
env: { ...process.env, REA_BUNDLE_NODE: process.execPath },
|
||||
});
|
||||
}
|
||||
|
||||
test("the smoke step is skipped when this run resolved no engine", () => {
|
||||
const root = mkdtempSync(path.join(tmpdir(), "rea-smoke-skip-"));
|
||||
try {
|
||||
const run = runSmokeSetup(root);
|
||||
assert.match(run.stdout, /\[skip\] smoke — this run resolved no ghidra \+ jdk pair/);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("--skip-smoke turns the smoke step off outright", () => {
|
||||
const root = mkdtempSync(path.join(tmpdir(), "rea-smoke-off-"));
|
||||
try {
|
||||
const run = runSmokeSetup(root, ["--skip-smoke"]);
|
||||
assert.match(run.stdout, /\[skip\] smoke — disabled by --skip-smoke/);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("the smoke step drives a real import and fails the run when it fails", () => {
|
||||
// Contract assertions rather than a live Ghidra run: CI has no 570 MB engine.
|
||||
// What must not regress is (a) the step really invokes REA's analyze on a file
|
||||
// this run owns, and (b) a non-zero verdict is a FAIL, not a silent skip.
|
||||
const source = readFileSync(SETUP_SOURCE, "utf8");
|
||||
assert.match(source, /\[reaEntry, "analyze", smokeInput, "--json"\]/);
|
||||
assert.match(source, /step\(\s*"smoke",\s*"FAIL",/);
|
||||
assert.match(source, /path\.join\(paths\.jdk, "bin", `java\$\{EXE\}`\)/);
|
||||
// The two variables the mirrored shim exports must reach the probe and the
|
||||
// smoke alike; without them both fell back to probing the machine.
|
||||
assert.match(source, /GHIDRA_INSTALL_DIR: paths\.ghidra/);
|
||||
assert.match(source, /JAVA_HOME: paths\.jdk/);
|
||||
});
|
||||
|
||||
test("a reused component carries its own version into the record", () => {
|
||||
// Both reuse branches returned only `{path, how}`, so bundle.json recorded the
|
||||
// interpreter running setup for Node and the manifest's pin for a JDK/Ghidra —
|
||||
// a record that names a runtime but describes a different build, which is the
|
||||
// one thing `check-updates.ps1` exists to catch. A live fixture cannot cover
|
||||
// this: adoption runs the component's own probe, so a stub tree would have to
|
||||
// be a working compiler or analysis engine. Contract assertions instead.
|
||||
const source = readFileSync(SETUP_SOURCE, "utf8");
|
||||
// The adopted record is spread, not rebuilt from its path alone.
|
||||
assert.match(source, /return \{ \.\.\.reused, reused: true \};/);
|
||||
// ...and each installation is asked what it is, from its own declaration.
|
||||
assert.match(source, /version: readJdkVersion\(home\)/);
|
||||
assert.match(source, /version: readGhidraVersion\(install\)/);
|
||||
assert.match(source, /function readJdkVersion\(root\)/);
|
||||
assert.match(source, /function readGhidraVersion\(root\)/);
|
||||
});
|
||||
|
||||
/* =========================== this round's regressions =========================
|
||||
*
|
||||
* Each test below pins a defect that was verified in the code before it was
|
||||
@@ -2049,3 +2125,28 @@ test("the guide asks for setup when the record cannot be parsed or names no REA"
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
/* ------------------------------------------------- npm run check-updates */
|
||||
|
||||
test("check-updates runs PowerShell on Windows, 5.1 first", () => {
|
||||
assert.deepEqual(powerShellCandidates("win32"), ["powershell", "pwsh"]);
|
||||
});
|
||||
|
||||
test("check-updates refuses to fake a POSIX run", () => {
|
||||
// The .ps1 probes %APPDATA% and prefers rea.cmd; a pwsh on Linux would run
|
||||
// those branches against paths that do not exist and print a confident wrong
|
||||
// answer. No candidates is the point, not an oversight.
|
||||
assert.deepEqual(powerShellCandidates("linux"), []);
|
||||
assert.deepEqual(powerShellCandidates("darwin"), []);
|
||||
});
|
||||
|
||||
test("check-updates names the portable commands it falls back to", () => {
|
||||
const posix = unsupportedMessage("linux");
|
||||
assert.match(posix, /Windows PowerShell tool/);
|
||||
assert.match(posix, /this host is linux/);
|
||||
assert.match(posix, /--shim-status/);
|
||||
assert.match(posix, /setup\.mjs --dry-run/);
|
||||
|
||||
const win = unsupportedMessage("win32");
|
||||
assert.match(win, /No PowerShell/);
|
||||
});
|
||||
@@ -12,7 +12,7 @@ function Check($name, [bool]$ok, $detail) {
|
||||
}
|
||||
|
||||
Write-Host "`n=== 1. JS syntax ===" -ForegroundColor Cyan
|
||||
foreach ($f in @('index.mjs','bin\rea-shim.mjs','bin\rea-guide.mjs','client.js','scripts\setup.mjs','scripts\audit-manifest.mjs','scripts\setup.test.mjs','scripts\host-compat.mjs')) {
|
||||
foreach ($f in @('index.mjs','bin\rea-shim.mjs','bin\rea-guide.mjs','client.js','scripts\setup.mjs','scripts\audit-manifest.mjs','scripts\setup.test.mjs','scripts\host-compat.mjs','scripts\check-updates.mjs')) {
|
||||
$out = & node --check $f 2>&1
|
||||
Check $f ($LASTEXITCODE -eq 0) ($out -join ' ')
|
||||
}
|
||||
@@ -229,6 +229,21 @@ try { $guideParsed = ($guideRaw -join "`n") | ConvertFrom-Json } catch { $guideP
|
||||
Check 'bin/rea-guide.mjs answers with JSON' ($null -ne $guideParsed -and $null -ne $guideParsed.needsSetup) (($guideRaw | Select-Object -Last 2) -join ' ')
|
||||
Check 'the guide command points at this package scripts' ($null -ne $guideParsed -and [string]$guideParsed.setupCommand -match 'scripts[\\/]setup\.mjs') ''
|
||||
|
||||
Write-Host "`n=== 10. npm run check-updates is portable ===" -ForegroundColor Cyan
|
||||
# check-updates.ps1 is Windows-only on purpose (it reads %APPDATA% and prefers a
|
||||
# rea.cmd shim over the rea.ps1 one). The npm script used to BE the bare
|
||||
# `powershell -File` command, so on macOS/Linux `npm run check-updates` was an
|
||||
# exec error, and a pwsh there would have answered from paths that do not exist.
|
||||
# The wrapper is the contract: Windows forwards, everything else explains and
|
||||
# exits non-zero.
|
||||
$wrapper = Join-Path $root 'scripts\check-updates.mjs'
|
||||
Check 'scripts/check-updates.mjs exists' (Test-Path $wrapper) ''
|
||||
$checkScript = [string]$pkg.scripts.'check-updates'
|
||||
Check 'scripts.check-updates goes through the wrapper' ($checkScript -match 'check-updates\.mjs') $checkScript
|
||||
$wrapperText = if (Test-Path $wrapper) { Get-Content $wrapper -Raw -Encoding UTF8 } else { '' }
|
||||
Check 'the wrapper runs PowerShell on Windows only' ($wrapperText.Contains("platform === 'win32' ? ['powershell', 'pwsh'] : []")) ''
|
||||
Check 'the wrapper names both portable fallbacks' ($wrapperText.Contains('--shim-status') -and $wrapperText.Contains('--dry-run')) ''
|
||||
|
||||
Write-Host ''
|
||||
if ($fail -eq 0) { Write-Host 'ALL CHECKS PASSED' -ForegroundColor Green }
|
||||
else { Write-Host "$fail CHECK(S) FAILED" -ForegroundColor Red }
|
||||
|
||||
Reference in new issue
Block a user