The third audit cleared the four P0s and the npm-fallback change, and rejected 2.0.15 on one point: the Frida gate hashed AFTER it imported. `verify()` — which imports frida and enumerates devices — ran before `judgeBinding()` compared the digest, so a mismatched native module was executed inside the provisioning process and then reported as rejected (the audit measured `moduleImportedBeforeRejection: true` on a Linux copy). Writing `available: false` afterwards does not undo an import, and the shim does not consume that field either, so nothing stopped REA from loading the same file on the next tool call. - `inspect()` is now the first thing that touches the binding: path + SHA-256, no import. `verify()` only runs when the digest matches the recorded one, or when this platform has no expectation at all (then `verified: false`, as before). - after `npm rebuild frida`, the result is inspected BEFORE it is imported too — a rebuild can produce a binding that exists, loads, and is not the measured one. - a mismatch now MOVES the file to `frida_binding.node.rejected` instead of only recording a verdict: REA reaches `build/frida_binding.node` directly, so taking it off that path is the only thing that actually stops the load. Quarantined rather than deleted, so the mismatch stays inspectable; if the move itself fails the message says so and tells the operator to remove the file by hand. - the message is now "moved it to … before any import, so nothing can load it", which is what the code does — the old "rather than loading an unverified native module" was the line 2.0.15 could not back up. Two regression tests, both injecting a frida package whose `index.js` writes a marker and then throws, so "was this module imported?" is a fact on disk: - the mismatched binding is never imported (no marker, output says "before any import"); - the binding is gone from the load path and present as `.rejected`. Both skip on targets without a pinned digest — the only measured one is `17.23.2/win32-x64` — and the skip message says the reason is "no expectation to mismatch", not that the platform does not matter. Tests: 160 (157 pass / 0 fail / 3 skipped). audit 24/24 hash-pinned clean, dependency gate clean, host-compat 16 covered, verify-branch ALL CHECKS PASSED. On this machine `--reuse` reports `fridaBindings — FRIDA_OK 3 — digest matches the recorded one`, the Ghidra smoke imports java.exe in 31 s, exit 0. Still open, unchanged from the last round: the shim rewrites the lock from its parent process without a token comparison (low-priority race, not release blocking), and Frida has a pinned digest for win32-x64 only.
dsh-plugin-rea
English · 简体中文
Which version is this? 2.x — and
mainis it. 2.x provisions the runtime itself (Node, the REA CLI, a JDK, Ghidra) and then wires the row: install the plugin, runscripts/setup.mjsonce, restart dsh. 1.x is the older configuration-only adapter — it expectsreaon PATH and Ghidra installed by you — kept on thev1branch and on npm as[email protected]. Full comparison: Two lines: 1.x and 2.x · upgrading: Upgrading from 1.x.
REA 6.8.0 — this package's pin — still accepts Ghidra 12.1.x only (12.2 is
unsupported, and the whole Ghidra decompile/analysis group disappears) and the jadx bridge 0.7.1 only (0.8.0 fails on its first call withExpected engine 0.7.1, before any APK is loaded). Both gates were re-checked against 6.8.0 on 2026-10-26 and neither one moved. JDK 21.0.13 is out upstream, but Adoptium publishes no Windows build of it, so there is nothing to point at here — and it drops only three of the four eMudhra anchors. The trade-off is therefore "all tools green, CVE still open": Ghidra 12.1.4's CVE-2026-100504 / 100505 / 100503 / 113604 stay in the chain — do not analyse untrusted samples with this environment — and the JDK still trusts the eMudhra anchors. Evidence, mitigations and revisit triggers: docs/update-review-2026-10-24.md and docs/update-review-2026-10-26.md.Ghidra 12.2 itself works — measured 2026-10-26: same 9 segments, same 341 procedures, byte-identical
entrypseudocode as 12.1.4, through REA's own bridge. What blocks it is REA's version-equality check, not the bridge. If you want the CVE-fixed engine anyway,node scripts/ghidra-gate-override.mjs --enable --version 12.2rewrites that one constant in the vendored copy, records what it changed in<bundle>/ghidra-gate-override.json, and--disablerestores it; re-running setup reverts it too. It needs JDK 25+ (application.java.min=25).
You must run
node scripts/setup.mjsyourself — once after installing, and once after every plugin upgrade. Nothing runs it for you: a market install or update replaces the package and stops there. Since 2.0.8 a market upgrade keeps the row working (the mirrored launcher re-execs the new one), but it does not move the bundle to the new pins — REA and the engines stay at the versions recorded by the last setup run. Skip it on a first install and the row exitsMODULE_NOT_FOUNDwith no tools and no message; skip it after an upgrade and you keep running the old REA silently (check-updates.ps1reports the drift). If you had enabled the Ghidra gate override (see §9 of the 2026-10-26 review), that setup run re-provisions the bundled REA and the patch goes with it — re-enable it afterwards, or accept the pinned Ghidra again. The full rule set is under Updating.
A first install and every plugin upgrade also show a one-time guide in the Web GUI. It says whether the runtime is missing or behind this package's pin, shows the exact
node "<install path>/scripts/setup.mjs"command for that install, and remembers the dismissal per plugin version. It is a notice only: it never runs setup for you and never edits the bundle. Run it once, restart dsh, and the guide stays away until the next plugin version.
Brings the upstream REA — Reverse Engineer Anything MCP server into
DeepSeek Harness (dsh), so its tools show up in a session as mcp__rea__*.
What this is
A Cordis bundle with a provisioning layer. package.json declares dsh.bundle.patch, and that patch
inserts one row of the official MCP client plugin (@deepseek-ai/dsh-mcp-client). The row launches
bin/rea-shim.mjs with the running Node executable; the shim reads the machine-local state that
scripts/setup.mjs wrote and starts the REA CLI it provisioned. There is no analysis code here, and the
published patch contains no machine path.
Third-party adapter. This repository is not part of REA, ships none of REA's code, and is not maintained by REA's authors. It composes one configuration row and fetches upstream's own releases. Upstream: github.com/morluto/rea (MIT) · docs · rea.tools · npm package
rea-agents. REA's own problems go to its tracker; this plugin's problems go to this repository's issues.
It adds exactly three things: the tool registration under the mcp__rea__* names, the per-machine
engine environment (GHIDRA_INSTALL_DIR, JAVA_HOME, REA_ADB_PATH, REA_APKTOOL_COMMAND,
REA_JADX_MCP_JAR, REA_BROWSER_EXECUTABLE, …) scoped to that MCP child instead of the whole harness, and
the provisioning that puts those components on disk.
What it can and cannot do
The analysis is upstream's; this section only restates upstream's own contract so expectations are right.
- It cannot recover complete source code. For native binaries upstream returns pseudocode and assembly, for Android APKs decompiled methods, for JEB units pseudocode, and for JavaScript/Electron modules and their relations. Upstream's tool contract states the pseudocode "is decompiler output, not original source and not text-equivalent" to another provider's output.
- Not recoverable in practice: original identifier names, types, comments, macros and template expansion, build scripts and assets. Optimisation (inlining, reordering), symbol stripping, obfuscation, packing and VM protection only make the output worse.
- Every result carries confidence and limitations (
observed/derived/inferred, plus an explicit limitations list). Read them as evidence, never as recovered source. - Good for: understanding behaviour, locating an implementation, interoperability and compatible re-implementation, security review, migration reference. To obtain actual source code, use the legitimate route — the upstream repository, vendor authorisation, or the vendor's own SDK.
What provisioning cannot buy you. These are upstream host gates, not settings this plugin forgot:
| Capability | Gate |
|---|---|
| Hopper provider | macOS only |
| IDA provider | needs your own IDA and REA_IDA_MCP_CONFIG; REA never launches IDA |
firmware binwalk / unblob |
Linux only |
| Wakaru JS recovery, pwntools ELF/recorded-crash paths | Linux x64 only |
| pwndbg / gdb core context | Linux, macOS |
| native mitmproxy captures | Linux (REA_MITMDUMP_COMMAND); HAR needs no engine |
| Apple toolchain (Mach-O, plist, asset catalog, LLDB) | macOS only |
capture_process_scenario |
Linux/macOS PTY |
| apktool (Android resource decoding) | available only when REA_APKTOOL_COMMAND points at something Node can execute directly: upstream execFiles that command without a shell, and Windows cannot execute a .bat/.cmd that way (EINVAL). This bundle wires the official apktool.bat as upstream documents, so on Windows the launcher reports no version and the capability stays unavailable; JADX (a jar) is unaffected |
Two lines: 1.x and 2.x
2.x — this line, main |
1.x — v1 |
|
|---|---|---|
| What it is | the adapter plus provisioning: it prepares Node, the REA CLI, a JDK and Ghidra, then wires the row | configuration-only adapter: one MCP row pointing at a rea you installed yourself |
| What you need first | nothing for the runtime — provisioning fetches what is missing (with --reuse it adopts an existing Ghidra/JDK instead) |
Node.js on REA's engines, npm i -g rea-agents, and Ghidra 12.1.x for native analysis |
| First run | run scripts/setup.mjs (or the bootstrap) once, then restart dsh |
nothing to run |
| The row launches | the running Node executable plus <DSH_HOME>/plugins/rea-bundle/bin/rea-shim.mjs, which reads that machine's state |
cmd.exe /c rea mcp on Windows, rea mcp on macOS/Linux |
| Machine state | <DSH_HOME>/plugins/rea-bundle/ — bundle.json, vendor/, cache/ |
your own REA and Ghidra installs |
| Install | dsh plugin --profile <profile> add dsh-plugin-rea (npm latest ≥ 2.0.0) |
… add [email protected], or github:czcdr/dsh-plugin-rea#v1.0.6 |
Both lines ship the same patch shape — one insert row of @deepseek-ai/dsh-mcp-client — and offer the
same mcp__rea__* surface; they differ in who prepares the runtime. Moving from 1.x to 2.x takes two
commands: see Upgrading from 1.x.
Upstream also registers itself with dsh now (rea setup --client dsh); that route is essentially the
1.x model maintained upstream — configuration only, your own Node/REA/Ghidra. This bundle is the
zero-dependency route. Pick one of the three, and never keep two serverName: rea rows at once.
Install
Nothing to install first. On a bare Windows x64 machine:
powershell -ExecutionPolicy Bypass -File scripts\rea-bundle-bootstrap.ps1 -Reuse
-Reuse adopts a Ghidra/JDK you already have; omit it to download both (about 775 MB from the official
releases, checksum-verified). Then install the plugin for your profile:
dsh plugin --profile <profile> add dsh-plugin-rea
# or pin this release instead of tracking npm latest:
dsh plugin --profile <profile> add github:czcdr/dsh-plugin-rea#v2.0.16
On an Electron host (the DeepSeek Harness desktop app) the profile is owned by the application.
dsh plugin --profile desktop …is refused by the CLI (profile "desktop" is managed exclusively by the Electron application) — install or update through the in-app plugin manager instead. The CLI form above is for a profile you created yourself with the CLI under another name.
If Node is already installed, you can skip the bootstrap and run provisioning directly:
node scripts/setup.mjs --reuse # or --dry-run to see the plan first
Node, when you provide it instead of letting setup fetch one, must satisfy REA's engines —
^22.23.4 || ^24.22.1 || >=26.12.1 (check with node -v). REA raised those floors in 6.8.0: the pinned
24.22.1 is now the oldest acceptable 24.x, so a 24.11–24.21 runtime that 6.7.0 accepted is rejected here.
A lower Node does not fail at install time; the MCP row fails later, which reads as silence.
Installing the plugin first also works, but it takes two steps. The row resolves to
<DSH_HOME>/plugins/rea-bundle/bin/rea-shim.mjs, and that file is created by setup — so until the first
provisioning run the row exits with MODULE_NOT_FOUND, and nothing is there to start a background
provisioning (the harness itself is unaffected: failOnStartupError: false is deliberate). Run
node scripts/setup.mjs once; from then on the shim is in place, and every later missing state is picked up
by the shim itself, which provisions in the background.
Which DSH versions
package.json declares engines.dsh: ">=0.1.7-rc.2 <0.3.0-0", and that declaration is the answer the plugin market
reads: with it the card says compatible; without it the market can only say unknown. The range is not a guess —
it is the dsh release list this plugin is checked against, and node scripts/host-compat.mjs fails the build when the
two disagree:
| Host | Covered |
|---|---|
0.1.7-rc.2 … 0.2.2-rc.1 — every release in between, alpha and rc (16 tags) |
yes |
0.3.0 and later |
no — a new minor line is a host nobody updated this row for yet |
2.0.14 checked the six tags published after 2026-10-23 individually (0.2.1-rc.3, 0.2.1-rc.4, 0.2.2-alpha.2,
0.2.1-rc.5, 0.2.2-alpha.3, 0.2.2-rc.1): none of their release notes touch the five surfaces this plugin depends on
(patch dialect, MCP row schema, host plugin API, client injection contract, market engines verdict). The one adjacent
item is 0.2.2-rc.1's Cordis Core 4.1.0 / Loader 1.1.0, and it was measured on this machine:
GET /dsh-plugin-rea/guide-state answered 200 and the mcp__rea__* tools were available.
What was actually compared across that range is the row provider: packages/mcp/mcp-client at dsh-v0.1.7-rc.2,
dsh-v0.2.1-rc.2 and dsh-v0.2.2-rc.1 exposes the same config surface the shipped row uses (serverName, transport,
command, args, env, failOnStartupError), and dsh.bundle.patch exists at all three. A host outside the range can
still work; it is simply not claimed.
The market reads
engines.dshonly, and notengines.node(measured indshmarket/lib/verify.jsanddiscovery-compatibility.js). A host whose bundled Node sits below this package'sengines.nodefloor is therefore not blocked by the market; only someone running npm withengine-strict=trueis stopped at install time. Run time is unaffected either way: REA is started with thepaths.noderecorded inbundle.json, not the host's Node.
A REA you already have is used as-is
2.x exists to supply what a machine is missing. If REA is already installed — rea on PATH, the 1.x setup — the row
runs that, exactly as the 1.x line did, and nothing is provisioned:
| What the machine has | What the row runs |
|---|---|
rea on PATH, at least as new as the provisioned build |
your REA (rea mcp), with your Node and your PATH |
rea on PATH but older than the provisioned build |
the provisioned bundle — a stale global CLI would take the tool count backwards (6.4.0 ships 170 tools, 6.7.0 ships 207, 6.8.0 ships 229) |
no rea on PATH |
the provisioned bundle, provisioning itself on first use |
REA_BUNDLE_LAUNCH=auto|system|bundleoverrides the decision (autois the default);REA_BUNDLE_SYSTEM_REA=<path>names a binary that is not on PATH.- The probe is the CLI's own
--version, bounded at 8 s: a.cmdshim on Windows takes ~2.4 s to answer, so the check runs once per start and only when areaactually exists. --shim-statusprints the decision —launch.mode,launch.reason,launch.systemRea— and itslaunch.rejectedlist says why a candidate was skipped ("exit 7","inside the bundle","timeout").- A delegated child carries
REA_BUNDLE_DELEGATED=1, so a wrapper that calls this shim cannot make it delegate to itself forever. - On Windows a forced kill of the shim does not reap the delegated REA. There is no job object here: Task Manager
or a supervisor terminating the shim leaves your
rearunning with the row's pipes (a console Ctrl+C reaches the whole process group and does stop it). On POSIX the child is in the same group and dies with the terminal.
Configure — nothing
The shipped row carries no machine path:
- insert:
- id: mcp-rea
name: '@deepseek-ai/dsh-mcp-client'
config:
serverName: rea
transport: stdio
command: !!js process.execPath
args:
- !!js "(process.env.DSH_HOME && process.env.DSH_HOME.trim() !== '' ? process.env.DSH_HOME : (process.env.USERPROFILE || ((process.env.HOMEDRIVE || '') + (process.env.HOMEPATH || '')) || process.env.HOME || '') + '/.dsh') + '/plugins/rea-bundle/bin/rea-shim.mjs'"
- mcp
env:
ELECTRON_RUN_AS_NODE: '1'
failOnStartupError: false
Three details in that row are load-bearing, and all three were bugs first:
- The
!!jsscalar 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_SCHEMAplus the!!jstype fromapp.asar), the unquoted form madeargs[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_HOMEfallback is<home>/.dsh, not<home>. The shim,setup.mjsandbootstrap.ps1all resolve an unsetDSH_HOMEto<home>/.dsh/...; a fallback that stopped one level higher made the row point at a file provisioning never writes — invisible on any machine whereDSH_HOMEhappens to be set. - The home fallback mirrors
os.homedir():USERPROFILE, thenHOMEDRIVE+HOMEPATH, thenHOME. The middle pair is what a Windows service account gets instead ofUSERPROFILE; an expression that skipped it produced/.dsh/...— the root of the current drive — for exactly the accounts that cannot read that path. Only when all four are empty (a container account with no home at all) does the concatenation land on/.dsh/..., andfailOnStartupError: falsekeeps that quiet, so the only symptom is "nomcp__rea__*tools". SetDSH_HOMEexplicitly on such an account. - No
path/requirein a!!jsexpression:ctxis 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.
ELECTRON_RUN_AS_NODE only matters if the harness itself runs on Electron; it is harmless for a plain Node host, and stating it explicitly beats relying on it surviving the child-env scrub.
command is the running Node executable and args[0] is computed from DSH_HOME, so the same patch works
on Windows, macOS and Linux without a PATH entry. Everything machine-specific lives in
<DSH_HOME>/plugins/rea-bundle/bundle.json, written by setup and readable with:
node bin/rea-shim.mjs --shim-status # resolved paths, versions, verification state
node bin/rea-shim.mjs --shim-print-env # the environment the MCP child will see
Both commands answer with the same bundle whether you run them from the plugin directory (the checkout copy)
or from <DSH_HOME>/plugins/rea-bundle (the mirrored copy): the checkout resolves the location by the same
rule setup uses, while the mirror uses its own position — it cannot rely on DSH_HOME, which the harness
strips from an MCP child's environment.
Setting REA_BUNDLE_AUTO_PROVISION=0 disables the first-run background provisioning.
To pin a different launch command on this machine (for example your own globally installed rea), override
the row from the profile's own cordis.patch.yml — that file is machine-local and never published. Only one
row may use serverName: rea in a profile.
A patch replaces the row's config wholesale — it does not deep-merge — so a partial override such as
config: {env: {...}} also deletes command, args and serverName, and the row stops starting. Supply the
complete config, or use REA_BUNDLE_LAUNCH=system (above), which needs no patch at all.
Ghidra and the JDK are not read from your environment at launch: setup.mjs records their absolute paths in
bundle.json and the shim exports GHIDRA_INSTALL_DIR / JAVA_HOME from that record — which is why a
non-standard install location works without touching the row. To adopt an existing install, point
GHIDRA_INSTALL_DIR / JAVA_HOME at it and run node scripts/setup.mjs --reuse: the manifest's probe checks
the candidate and falls back to a download when it does not match, so a wrong path is reported during setup
instead of failing later inside a tool call.
One REA registration only
Upstream REA 6.5.0 can register itself with DSH:
rea setup --client dsh
That writes a row with id: rea-mcp and serverName: rea into <DSH_HOME>/cordis.patch.yml. This
bundle's row is id: mcp-rea with the same serverName, and DSH rejects the later of two entries
that share a server name — so which REA row survives depends on load order. Use one, not both.
Which one? Upstream's is the configuration-only route: it expects Node on REA's engines,
rea-agents reachable, and a Ghidra/JDK you provisioned yourself. This bundle is the
zero-dependency route: it fetches Node, REA, JDK and Ghidra on first run. The patch shape and the
mcp__rea__* surface are the same either way.
The shim reports the conflict on startup — setup.mjs records the DSH home it ran under in
mirror.json, and the mirrored copy reads that when the harness has stripped DSH_* from its
environment; node bin/rea-shim.mjs --shim-status lists it under duplicateRegistration.
Verify
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 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 / 23 / 26:
| Component | Version |
|---|---|
REA (rea-agents) |
6.8.0 (229 tools) |
| DeepSeek Harness | desktop runtime 0.2.2-rc.1 (the host 2.0.14 was measured on; full range in the table above) |
@deepseek-ai/dsh-mcp-client (row provider) |
bundled by the harness |
| Ghidra | 12.1.4 (this machine also runs 12.2 through the gate override — see below) |
| JDK | Temurin 21 (provisioned) / Amazon Corretto 25.0.5 (adopted; its major satisfies the pin 21) |
| Node.js | 24.22.1 adopted (REA engines: ^22.23.4 || ^24.22.1 || >=26.12.1; ABI 137) |
| Host | Windows 11 x64 (build 26200) |
Exercised live: a clean setup.mjs --reuse run reached setup complete; an MCP initialize handshake
through the shim with no rea on PATH answered serverInfo {name: rea, version: 6.8.0}; and the whole
chain ran under the real desktop host — its own Electron binary launched
<DSH_HOME>/plugins/rea-bundle/bin/rea-shim.mjs mcp, the shim started the vendored REA, and a live tool
call reported path_source: environment for a path only that machine's bundle.json knows.
Not exercised end to end here, and therefore not claimed: the ~775 MB JDK + Ghidra download branch (planned
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.
On 2026-10-26 the pin moved to REA 6.8.0 and the chain was re-verified on the same machine: setup.mjs --reuse reported probe cli=6.8.0 tools=229 and its Ghidra smoke imported java.exe in 26 s; an independent
MCP handshake over stdio answered serverInfo {name: rea, version: 6.8.0} with 229 tools / 6 prompts on
protocol 2024-11-05; apktool still answers 3.0.3, adb still reports 37.0.1 (1.0.41), and the jadx jar on
disk still hashes to 6e5eacf5… — the value REA 6.8.0 requires. Executing 6.8.0's own release policy gave
12.1.4 → verified, 12.1.5 → compatible, 12.2 → unsupported, so Ghidra stays where it was.
On 2026-11-06 (2.0.14 — an 18-item external audit plus host adaptation) no upstream component moved (REA 6.8.0 /
Ghidra 12.1.4 / JDK 21 / apktool 3.0.3 / jadx 0.7.1 / Node 24.22.1). The same chain was re-verified: --dry-run
skipped all 7 components on their pins (the Ghidra 12.2 gate override and JDK 25 were not mistaken for drift) →
a forced apktool re-provision went through the full staging path (no leftover .staging/.backup, and the directory
held exactly the two files the manifest declares) → a full --reuse run covered 8 components plus
fridaBindings FRIDA_OK 3 → probe cli=6.8.0 tools=229, the Ghidra smoke imported java.exe in 28 s, and the shim
resolved 6.8.0, exit 0. Gates: 154 tests (151 pass / 0 fail / 3 skipped), audit 24/24 hash-pinned clean,
the dependency gate at zod 4.6.7 · minimatch 11.0.5 · glob 14.0.4, host-compat 16 releases, and
verify-branch ALL CHECKS PASSED. check-updates.ps1 went from a false-green exit 0 to reporting honestly, and exits 0
on this machine once repaired.
Scope and caveats
- REA exposes 229 tools at 6.8.0, so every session's tool list grows.
- Provisioning is honest about checksums: where a vendor publishes one it is pinned and verified; where none
exists (jadx-headless-mcp, the apktool wrapper, Android platform-tools, the rolling "latest" URLs) the
hash is measured once and recorded, and later drift is reported rather than silently accepted.
bundle.jsonlists which components are in that state underunverifiedChecksums. - Downloads are slow on some networks (measured 0.35–0.54 MiB/s from the GitHub release CDN here, so Ghidra
alone takes ~25–45 min). Fetching is resumable on both routes: a partial
.partis continued with a Range request, and a.partthat already holds the whole file is verified and kept instead of re-fetched. - Some REA tools launch processes, write files and access the network — DSH's permission policy applies. Analyse only artifacts you are authorized to inspect.
- The MCP child and the engines it launches are ordinary processes running as you. DSH's tool approvals govern what the model asks for, not what that child may read, write or send on its own; treat the plugin like any third-party code you install.
- On Linux, upstream's process-ownership checks need a procps-compatible
pson PATH; the Ghidra/JADX and firmware paths use it. A minimal image without it fails those tools, not the install. scripts/setup.mjsresolves the REA CLI's closure withnpm ciagainst the shippedscripts/vendor-lock/(package.json+package-lock.json). REA pins zod exactly ("4.4.3"), and 4.4.3 is inside GHSA-mrhx-c7xc-m3w5, so the closure is raised throughoverridesto zod 4.6.7 / minimatch 11.0.5 / glob 14.0.4.npm civerifies every tarball's sha512; if a repackaging mirror makes that fail withEINTEGRITY, it falls back to "same versions via overrides +npm install" (same versions, not byte-reproducible, and the log says so). After landing, the version innode_modules/zod/package.jsonis read once more and anything below 4.6.6 is refused.node scripts/check-dependency-advisories.mjsis the offline gate, wired into CI. Package lifecycle scripts are still skipped on purpose — with exactly one named exception: frida, whose native binding (build/frida_binding.node) only exists after its install script runs, so setup runsnpm rebuild fridafor that one package and then proves it withimport('frida')plus device enumeration (recorded undercomponents.reaAgents.fridainbundle.json). A missing binding makes the Frida-backed tools unavailable and the rest of REA untouched, so it is reported asskiprather than failing the whole provision. This step is also the only one that cannot use this bundle's own route/proxy resolution — npm readsHTTPS_PROXY/HTTP_PROXYandNPM_CONFIG_REGISTRYfrom the environment, so on a network that needs a mirror, set one of those before provisioning (NPM_CONFIG_REGISTRY=https://registry.npmmirror.com). With--offlinethe step is refused outright instead of quietly going online.- On POSIX, extraction falls back when the usual tools are missing: a zip is opened with
unzip, thenpython3 -m zipfile, then the provisioned JDK'sjar; a tarball withtar, thenpython3. A minimal image that has none of them fails with an explicit "no zip extractor available" line rather than a bare "extraction failed". This matters because Ghidra is a zip on every platform andunzipis not installed everywhere by default.
Updating — three independent things
Two standing rules — for this release and for every later 2.x release.
setup.mjsreconciles the bundle, never the plugin: it reads the manifest of the plugin that is installed right now. Installed 2.0.9? setup provisions 6.5.0 even after a newer release exists. The package changes only through the market (ornpm i -g dsh-plugin-rea@<version>/ afile:reinstall); running setup more often never substitutes for that.And nothing runs it for you. A market install or update replaces the package and stops; the first
setup.mjs— and one after every upgrade — is yours to run, from the plugin directory.
- The row executes a copy of this plugin's launcher.
<DSH_HOME>/plugins/rea-bundle/bin/rea-shim.mjsis a mirror made by setup, and a market upgrade replaces the package without replacing it — so the mirror can keep running old code with no symptom at all. Since 2.0.8 the mirror compares itself with the launcher recorded inmirror.jsonand re-execs the plugin's copy when the two differ (one hop, guarded byREA_BUNDLE_SHIM_REEXEC). A mirror older than 2.0.8 cannot do this, so upgrading from 2.0.0–2.0.7 still needs onenode scripts/setup.mjs.--shim-statusreportsmirror.stale;check-updates.ps1says when to re-run.- The mirrored
scripts/setup.mjsis a copy too, and it has no self-heal of its own. Running setup from the bundle root therefore executes the version from when the bundle was created, not the one you just installed. Run it once from the plugin directory to refresh that copy. Since 2.0.9 the copy strips the UTF-8 BOM frommirror.json/bundle.jsonexactly as the shim does, so a BOM-ed record no longer reads as "no record" on the setup side (which used to re-mirror the previous manifest).
Updating from the market
Update it where you installed it. Updating to 2.0.5 or later is routine; updating an installed 2.0.1–2.0.4 through the in-app market failed with this, three times on a healthy profile:
dsh-plugin-rea 更新后缺少入口文件(package.json 的 main/exports 指向的文件不存在),且未能验证恢复原版本文件
That message is not about a broken tarball. On every update the market asks whether the new build's
package.json points at an entry file that exists (hasLoadableEntry in dshmarket), and rolls the build back
when the answer is false. A bundle may legitimately own no entry — but only if a package it mounts has one, and
the package this bundle mounts (@deepseek-ai/dsh-mcp-client) ships inside the harness build rather than in
node_modules, so every location that test probes comes back empty. The 1.x and 2.0.x manifests also declared
no main at all, so the answer was false even though the profile was working; the rollback was then reported as
unverifiable for the same reason. Nothing in the profile was actually damaged — the update simply did not apply.
2.0.5 declares main: ./index.mjs. That file is a real, deliberately empty Cordis plugin: it registers nothing,
spawns nothing, touches no state — the working row is still inserted by cordis.patch.yml — and it exists so the
question "can this package load?" has an honest answer, and so import 'dsh-plugin-rea' resolves. On an older
version, update to >=2.0.5; if the market refuses before that version exists, install [email protected]
(Desktop: through the plugin manager) instead of updating in place.
Upgrading from 1.x
Install 2.x, then run provisioning once before restarting dsh:
node scripts/setup.mjs # or: powershell -File scripts\rea-bundle-bootstrap.ps1 -Reuse
Why it matters: the 1.x row launched rea mcp from PATH, while the 2.x row launches
<DSH_HOME>/plugins/rea-bundle/bin/rea-shim.mjs — a file only setup.mjs creates. Upgrade and restart
without provisioning and that row exits with MODULE_NOT_FOUND: the harness stays up
(failOnStartupError: false is deliberate), but the mcp__rea__* tools are gone and you get no
remediation message, because the shim that prints one does not exist yet. After the setup run they are
back on the next start.
- This plugin never updates itself. Installed from a market? Update it there. Installed from a
file:checkout? Overwrite the directory and reinstall. - The row runs a COPY of this plugin's launcher.
<DSH_HOME>/plugins/rea-bundle/bin/rea-shim.mjsis a mirror made by setup, so a market upgrade replaces the package but not the file the row executes — until setup runs again. From 2.0.8 the mirror notices that the installed plugin ships a different launcher and re-execs it (one hop, guarded byREA_BUNDLE_SHIM_REEXEC), so upgrades carry. On an older version the mirror stays behind silently;--shim-statusnow reportsmirror.stale, andcheck-updates.ps1says to re-run setup. If you are upgrading from 2.0.0–2.0.7, runnode scripts/setup.mjsonce after the upgrade. - The mirrored
scripts/setup.mjsis a copy too. A market upgrade replaces the package but not that copy, so a remediation command that points at<DSH_HOME>/plugins/rea-bundle/scripts/setup.mjsruns the setup from when the bundle was created — not the one you just installed. From 2.0.9 both the shim and setup strip a UTF-8 BOM frommirror.json/bundle.json(a BOM-ed record used to read as "no record" on the setup side, which re-mirrored the previous manifest). Runningnode scripts/setup.mjsonce from the plugin directory refreshes that copy too. - Upgrading from
2.0.0-bundle.1: that release mirrored the shim andsetup.mjsbut not the manifest, and recorded nothing about the plugin checkout, so a bundle it provisioned cannot repair itself — its background provisioning dies on the missing manifest, invisibly, and its own lock suppresses retries for six hours. Re-runnode scripts/setup.mjsfrom the plugin directory once; it re-mirrors all three files and writes the record that lets later runs find the checkout.check-updates.ps1reports this state. - Provisioned components: re-run
node scripts/setup.mjs; it re-verifies what is on disk and only fetches what is missing or wrong. Pins live inmanifest/bundle.manifest.json. - 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. 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?
1.x is the adapter without provisioning: one row that launches rea mcp from PATH and expects Ghidra to be
installed by you. It is the right choice when you already manage REA and Ghidra yourself, when you do not want
the plugin fetching anything, or when you want the row to keep pointing at your own rea binary:
dsh plugin --profile <profile> add [email protected]
# or pin the branch instead of the version:
dsh plugin --profile <profile> add github:czcdr/dsh-plugin-rea#v1.0.6
Both lines publish the same patch shape and the same mcp__rea__* surface. 1.x is maintained on the
v1 branch, where its own install notes live (Node on REA's
engines, npm i -g rea-agents, Ghidra 12.1.x for native analysis).
License
MIT (this adapter). REA is MIT and owned by its authors; Ghidra is Apache-2.0 and owned by the NSA; Temurin is GPLv2+CE; Node.js is MIT; apktool, jadx-headless-mcp and Android platform-tools keep their own licences. No upstream code or binary is redistributed in this package — provisioning fetches each component from its official source.
