The market page renders `assets/screenshot-1.png` straight from GitHub HEAD, so
whatever is committed here is what shoppers see. It still said "DSH desktop runtime
0.2.0-rc.2" and "2026-10-26" — two months and several host releases behind what this
plugin was actually measured against.
- the host version became a variable (`$hostMeasured`); it has no manifest field to
read because it is not a property of this package, so it carries a comment saying
it must move with the measurement
- `$measuredOn` is 2026-11-07, the date of the last real `--reuse` run (smoke
imported java.exe in 31 s), and the stale hardcoded defaults (6.7.0 / 24.21.0)
were brought up to the current pins so a failed manifest read no longer paints
old numbers
- both PNGs regenerated; screenshot-2 carries no version text and is unchanged in
content
The npm tarball for 2.0.16 still holds the previous images — that is harmless, because
the market reads these files from HEAD, not from the package.
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.
A second audit of 5c71bf8 found 4 release-blocking gaps, all reproduced, all real.
Two are things 2.0.14 left undone (one of them documented as done), two are
defects 2.0.14 itself introduced. The common shape this time is the mirror of the
first round: the fixed path was not the path a user actually walks — rollback only
in the multi-file branch, closure floors only after a fresh install, the Frida
verdict written once, the old closure deleted before the new one was verified.
P0 — left undone by 2.0.14
- a plain archive had no rollback: the 2.0.14 note claimed "renamed to .backup,
rolled back if the probe fails", but that lived only in the multi-file branch and
`archive` still did rm -> rename -> return on a failed probe. Measured: exit 1,
old known-good tree gone, broken tree in place, no .backup. Both branches now
share the staging -> backup -> swap -> probe -> drop contract, and the message
distinguishes "put back", "parked at", "nothing to restore".
- the advisory fix never reached existing installs: the `recorded` branch and the
"already vendored" branch both returned before any closure check, so a 2.0.13 ->
2.0.14 upgrade printed `already provisioned (6.8.0)` with zod still 4.4.3.
`closureAdvisories()` reads both node_modules roots and runs before BOTH early
returns; below a floor it reinstalls, and offline it fails instead of reporting
health. Verified on this machine, which was in exactly that state: it reported
zod 4.4.3 < 4.6.6, minimatch 10.2.6 < 11.0.1, glob 13.0.6 < 14.0.2, reinstalled
from the audited lock to 4.6.7 / 11.0.5 / 14.0.4, and the closure has been
vulnerable-free since.
P0 — introduced by 2.0.14
- the Frida verdict was cached forever: gated on `frida === undefined`, one
transient failure became permanent (every later run read `available: false` and
skipped), and a binding deleted after a good record was never re-checked. The
check now runs on every non-dry-run.
- the swap deleted the old closure before verifying the new one: `.retired` was
removed one line after the rename, so a closure that renamed fine and then could
not start left the machine with no usable REA. `retired` now survives until
BOTH `--version` and `doctor --format json` answer, and `restorePreviousClosure()`
puts it back otherwise. doctor's verdict is "produced parseable JSON", not
"healthy" — it exits non-zero for any unconfigured optional capability.
P1
- `npm ci` no longer degrades silently to an unlocked install: that fallback traded
byte-reproducibility for a green run. It now fails closed with the reason, and
`--allow-unlocked-npm-fallback` is the explicit opt-in.
- Frida binding digests: FRIDA_BINDING_SHA256 holds the ONE measured value
(17.23.2/win32-x64). Match -> verified: true; no expectation for this platform ->
usable but verified: false and the step says the digest was recorded, not
checked; MISMATCH -> Frida disabled rather than loading an unmeasured native
module. "We hashed what we got" is no longer reported as a verified chain.
P2
- the run lock is only deleted or rewritten when it still carries this run's token
(or pid); otherwise it is left alone, because removing somebody else's lock is
how a live run loses its brake.
Also: the offline reinstall message now names which of the three reasons applies
instead of calling all of them "not usable".
Tests: 158 (155 pass / 0 fail / 3 skipped), including the four fault injections the
audit asked for — archive probe failure restores the old install, a recorded 6.8.0
with zod 4.4.3 is not reported as provisioned, a stale frida verdict is re-checked,
and the swap ordering contract (retired outlives both probes). audit 24/24
hash-pinned clean, dependency gate clean, host-compat 16 covered, verify-branch
ALL CHECKS PASSED.
The Windows runner came back red on three check-updates assertions for one
reason: `Get-FileHash` does not exist there, the mirror check threw, and the catch
reported `mirror.json 解析失败` as [注意] — which counts as "needs action". A host
that cannot hash two files therefore looked like a machine with a broken bundle.
That is the same false red this release already removed from the `rea doctor` exit
code and from the bare-major pin comparison, so it gets the same treatment:
- the mirror check asks `Get-Command Get-FileHash` first and says [上游] when the
capability is absent, rather than implying the mirror is inconsistent
- "mirror.json points at a plugin directory that no longer exists" and a
mirror.json that will not parse are also [上游]: both are "cannot compare",
not "compared and differs"
The test that constructs the stale-mirror warning now skips on a host without
Get-FileHash, because that scenario is unreachable there — asserting on a warning
the platform cannot emit would only produce another red that means nothing.
Verified: hardening.test 14/14 locally, full suite 154 (151 pass / 3 skipped).
`npm run bundle:deps` now runs after the manifest audit on Linux and on Windows,
so a lock that drifts back onto an advisory-affected zod/minimatch/glob — or onto
a rea-agents release the manifest does not pin — fails the build instead of only
being caught on the machine that provisioned it. The gate is offline, which is the
point: one that needs the registry fails on a plane and passes during an outage.
The earlier commit had to leave this out because the push credential had no
GitHub `workflow` scope; with a scoped credential it lands here.
The Windows CI job went red on five check-updates assertions while Linux passed.
Two separate causes, both in the test harness rather than the shipped script:
1. A Windows runner's console answers in CP437, where every CJK character
collapses to "?" irreversibly — the conclusion line arrived as
`[??] ????:pin ?? 1 ?;?? 2 ?(????)`, which decodeOutput's GBK fallback cannot
rescue. The child process now sets [Console]::OutputEncoding and
$OutputEncoding to UTF-8 before running the script. This is a harness concern:
the product's own output behaviour is untouched.
2. Replacing `-File` with `-Command` broke parameter binding. `& 'script.ps1'
'-Registry' 'url'` hands PowerShell a positional string "-Registry" instead of
binding the named parameter, so it became $Registry's value and the lookup
requested "-Registry/rea-agents/latest" (measured: HTTP 502). Parameter names
stay unquoted now; only values are quoted.
Both were caught by CI and reproduced locally before the fix, verified after:
hardening.test 14/14, full suite 154 (151 pass / 3 skipped / 0 fail).
An external audit of 2.0.13 found that install state could lie: a partially
failed install was recorded as a success, a broken recorded path was vouched for
by a different working copy, and an expired lock was deleted out from under a
live process. Every finding shares one shape — the thing verified was not the
thing being delivered — so the fix in each case is to make the probe look at the
artefact that will actually be used, and to keep "cannot prove" distinct from
"proved false". All 18 items are addressed. 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.
Provisioning (scripts/setup.mjs)
- multi-file components are all-or-nothing: every entry must arrive, files are
staged in <target>.staging and swapped in atomically only when complete, the
previous directory is kept as .backup and restored if the swap or the probe
fails, and the record carries one {id,version,sha256} per entry with the
primary chosen by kind instead of by manifest order
- a recorded component is probed AT its recorded path (independent resolve
context) and its version is checked against the manifest pin; JDK matches by
major floor; an enabled Ghidra gate override is honoured so a plain setup can
no longer silently re-pin over one
- JSON state files are written through writeJsonAtomic(); an unparseable
setup.lock is replaced instead of throwing; --dry-run writes nothing at all
- Frida's native binding is repaired by naming the one package whose install
script is load-bearing (npm rebuild frida) and then proved with
import('frida') + enumerateDevices(); checked in main, so existing bundles
(i.e. every upgrade) get fixed too, and reported as skip rather than failing
- the tar fallback passes filter='data' to Python's extractall
Runtime (bin/rea-shim.mjs)
- one definition of bundleUsable = loaded.ok && resolveRea(...).ok drives the
launch decision, the --shim-status exit code and the repair path; an unusable
bundle no longer wins against a working system REA, and status returns 78
- liveness outranks age for a running lock: a live pid is never deleted, an
expired one is reported as in-flight-stale; locks with no pid keep the old
time-based rule; setup heartbeats `at` from log(); shim->setup handoff uses a
token (REA_BUNDLE_LOCK_TOKEN) instead of a pid
Dependencies (scripts/vendor-lock/, scripts/check-dependency-advisories.mjs)
- REA pins zod exactly at 4.4.3, which sits inside GHSA-mrhx-c7xc-m3w5; the
closure is now replayed from an audited lock with npm ci and overrides to zod
4.6.7 / minimatch 11.0.5 / glob 14.0.4, verified afterwards against 4.6.6
- an offline gate fails the build if the lock drifts back, and CI runs it
Host adaptation (scripts/host-compat.mjs)
- HOST_RELEASES 10 -> 16: 0.2.2-rc.1, 0.2.2-alpha.3, 0.2.1-rc.5, 0.2.2-alpha.2,
0.2.1-rc.4, 0.2.1-rc.3, all three-source verified and in release-date order;
engines.dsh and OUTSIDE_HOSTS unchanged
Also fixed while verifying (not in the audit)
- a bare-major pin ("21") was treated as a family prefix, so 25.0.5 counted as
drift while setup counted it as satisfied; it is a floor, in all three files
- check-updates.ps1 no longer reports a false red: `rea doctor` exits non-zero
for any unconfigured optional capability, so that exit code is now [upstream]
and the parsed provider checks decide
- two new JSON reads went around the BOM-tolerant reader; routed through
readJsonSafe
Tests: 154 (151 pass / 0 fail / 3 skipped), audit 24/24 hash-pinned clean,
dependency gate clean, host-compat 16 covered, verify-branch ALL CHECKS PASSED.
Restarting dsh with the 2.0.12 copy still in the profile re-provisioned
the bundle to 2.0.12's pin: REA went back to 6.7.0, the Ghidra gate
override went with it, and the MCP row failed with "REA could not start".
That is the card doing its job on a machine whose plugin copy was stale —
but the card never said the aligned run also wipes the override.
- client.js: third note in both languages. It names the override case and
the command that restores it, so the card is not advice that silently
undoes a deliberate choice.
- README (both): the "you must run setup" block says the same thing.
- ghidra-gate-override.mjs: judge a recorded override by the files on disk.
Re-provisioning replaces rea-agents while leaving the record behind, so
--enable now replaces a stale record instead of refusing, and --disable
counts already-lost files instead of throwing.
- CHANGELOG: 2.0.13 records both, plus the failure that prompted them.
Verified after re-aligning this machine: setup --reuse reached
"setup complete" with its Ghidra smoke importing java.exe in 28 s on 12.2,
the shim resolves REA 6.8.0, and the MCP path through the shim returns the
same segments / procedure inventory / entry pseudocode as the 12.1.4
baseline with provider.version = 12.2.
Gates: audit 24/24 hash-pinned clean, 88 tests (85 pass / 3 skipped),
verify-branch ALL CHECKS PASSED, host-compat 10 covered / 5 outside.
The four CVEs in the chain are Ghidra 12.1.4's and are fixed in 12.2, and
12.2 is exactly the release REA 6.8.0 refuses: SUPPORTED_GHIDRA_VERSION is
still 12.1.4, and an out-of-line build withdraws the whole Ghidra family.
Measured today, so the decision rests on evidence rather than on the gate:
Ghidra 12.2 (ghidra_12.2_PUBLIC_20261016.zip, 569,712,708 B, sha256
2c2cd776... matching the release body) against 6.8.0's own bridge produced
the same 9 segments, the same 341-procedure inventory (74,483 chars) and a
byte-identical `entry` pseudocode as the 12.1.4 baseline, through the shim,
with provider.version = 12.2. The gate blocks a working combination; it does
not describe a broken one.
scripts/ghidra-gate-override.mjs therefore makes that choice explicit,
recorded and reversible: it rewrites only the constant in the vendored copy
(three files), writes what it changed to <bundle>/ghidra-gate-override.json,
and --disable restores the original values. Re-provisioning rea-agents
removes the patch, so the default after any setup run is the pinned,
verified combination again. 12.2 needs JDK 25+ (application.java.min=25),
which the adopted Corretto 25.0.5 satisfies here but the pinned Temurin 21
would not.
check-updates.ps1 now reads that record and reports "local override" instead
of advising a setup re-run, which is what would have silently undone it.
Gates: audit 24/24 hash-pinned clean, 88 tests (85 pass / 3 skipped),
verify-branch ALL CHECKS PASSED.
Also recorded: editing check-updates.ps1 stripped its UTF-8 BOM, which makes
PowerShell 5.1 read it as ANSI and fail to parse at all; the BOM is restored
and the note is in docs/project-notes.md.
REA 6.8.0 (npm latest, 2026-10-25) is a tool expansion, not a patch: 229
MCP tools instead of 207 (+22: APK signatures, binary diagnostics, model
artifacts, CUDA/HIP code objects), 175 CLI commands, 6 prompts, and a
shipped dist/mcp-catalog.json that records the counts and three digests.
It also raises engines.node to ^22.23.4 || ^24.22.1 || >=26.12.1, which
makes the pinned Node 24.22.1 exactly the floor of its line. The pin,
package.json engines and the node probe therefore move together: a probe
has no version comparator, so the range is one alternation verified
against 24 versions (22.23.3, 24.22.0 and 26.12.0 must be rejected).
Nothing else moved. 6.8.0 still declares SUPPORTED_GHIDRA_VERSION 12.1.4
(12.2 stays unsupported) and JADX_RELEASE 0.7.1 with the same sha256, and
Adoptium still publishes no Windows build of 21.0.13 — so Ghidra, jadx,
JDK, apktool, platform-tools and Node keep their pins.
Measured here, not inferred: setup --reuse reported cli=6.8.0 tools=229
and its Ghidra smoke imported java.exe in 26 s; an independent stdio MCP
handshake answered serverInfo 6.8.0 with 229 tools / 6 prompts on
protocol 2024-11-05; executing 6.8.0's release policy gave 12.1.4
verified / 12.1.5 compatible / 12.2 unsupported; apktool 3.0.3, adb
37.0.1 and the 0.7.1 jar hash all re-checked. Gates: audit 24/24
hash-pinned clean, 88 unit tests (85 pass, 3 skipped), verify-branch
ALL CHECKS PASSED, host-compat 10 covered / 5 outside.
Docs: both READMEs (versions, tool counts, engines sentence, tag example),
docs/bundle-design.md and the new docs/update-review-2026-10-26.md, which
also records the npm/proxy trap that made the first provisioning run fail
with ECONNRESET, and why WSL needs no separate update.
The paragraph below it already states what is pinned, why 12.2 / 0.8.0
are refused, and what the trade-off is; the one-line lead was redundant
and read as a judgement rather than a fact. No other change.
Adds docs/update-review-2026-10-24.md and one paragraph to both READMEs,
directly under the "which version is this" block.
Measured findings:
- Ghidra 12.2 is published (2026-10-22), but GhidraReleasePolicy.js accepts
the 12.1 line only: 12.2 resolves to "unsupported" and the whole Ghidra
decompile/analysis group disappears rather than degrading.
- jadx-headless-mcp 0.8.0 passes class admission but is rejected by
JadxSession.js:61, which requires the engine to report 0.7.1. Only
BuildInfo and JadxSession differ from 0.7.1; the other bridge classes are
byte-identical, so the break is the version assertion, not ABI.
- Temurin 21.0.13+9 ships 204 assets and none for Windows, so win32-x64 has
nothing to point at; it also removes only 3 of the 4 eMudhra anchors.
- REA 6.7.0, Node 24.22.1, apktool 3.0.3 and the platform-tools bytes are
already the newest upstream.
No versions pinned or moved; documentation only.
The one-shot setup card appears mostly after an upgrade, when the runtime is
already on the machine. Its command carried no `--reuse`, so following it
re-fetched the ~570 MB JDK + Ghidra pair — the opposite of "align the runtime",
and enough to turn a thirty-second step into fifteen minutes. The command now
reads `node "…/scripts/setup.mjs" --reuse`; on a first install the flag costs
nothing, because every candidate is probed and a machine with none downloads
exactly as before.
The card also states the two things that look like failures and are not: setup
ends by importing a small file to prove the engine works (32–42 s of silence),
and a concurrent run holds `setup.lock`, which makes setup exit 75 with the
holder's pid and log path.
The README install lines move to #v2.0.12, which verify-branch pins against
package.json.
audit-manifest — item 13 tested a string with `Number.isFinite`, which never
coerces, so the branch was always false and "does the probe accept the next
patch/minor" could not fail. Verified end to end with a probe of `^6\.`: the
audit now reports "accepts 6.7.1 (the next patch)" and "accepts 6.8.0".
rea-shim — a zero-argument start printed usage and exited 0, while both launch
paths still carried an unreachable `argv.length > 0 ? argv : ["mcp"]` fallback.
With `failOnStartupError: false`, that exit 0 leaves REA silently absent from
every tool list. The two cases are now told apart by whether a terminal is
attached: usage + 0 for a person, usage + stderr + EX_USAGE(64) for a harness
that lost its arguments. Both dead fallbacks removed.
setup.mjs — three:
* provisioning cleared the destination BEFORE extracting, so a missing
extractor or a full disk destroyed a working install and replaced it with
nothing; the tree is cleared only for the rename now.
* a `.part` holding the whole body could not be rescued when the manifest
declared no size — the Range request got 416 on every retry. A pinned
sha256 is now enough evidence to finish the rename.
* `setup.lock` was written only when a run ENDED, so it recorded an outcome
and excluded nothing: the shim's detached run and a hand-run setup could
write the same `.part` and the same staging directory. It is claimed with
O_EXCL before the first byte moves; a live holder is reported with its pid
and log path and the run exits 75, while a dead pid or a `failed` lock is
taken over.
check-updates.ps1 — a missing `application.version` line was reported as a
version mismatch ("requires 12.1.x, currently "), sending the reader after a
newer Ghidra instead of a reinstall.
rea-bundle-bootstrap.ps1 — the engines check compared only major/minor, so
`^24.11.1` accepted 24.11.0; all three components compare now.
rea-shim PATH dedupe — Windows compared path spellings case-sensitively and
could prepend the vendored Node twice.
cordis.patch.yml — the home fallback skipped HOMEDRIVE+HOMEPATH, so a service
account resolved `/.dsh/...`. It now mirrors `os.homedir()`: USERPROFILE →
HOMEDRIVE+HOMEPATH → HOME. Both READMEs follow, because the audit compares the
documented expression with the shipped one.
setup.mjs header — the TOFU sentence promised "enforced afterwards" while an
`unverified` checksum is trusted on a cache hit. The comment now states what the
code does and names the gap in `bundle.json` instead of implying verification.
Tests 85 -> 88: extraction order, the audit's item-13 branch, and a live lock
blocking a second run; the bare-invocation case asserts EX_USAGE plus the
`--shim-help` path that must stay 0.
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.
The setup step is mandatory but invisible: a market install or update replaces
the package and stops there, and a skipped setup fails silently in both
directions - no tools on a first install, the old REA after an upgrade. The
guide names the actual state instead of asking whether it was shown before.
- bin/rea-guide.mjs: computeGuideState compares the REA version recorded in
plugins/rea-bundle/bundle.json with the manifest pin and reports
missing / stale / unknown / unreadable / ok. Same pin rules as
scripts/pin-match.ps1 (dotted pin exact, bare major as a family), and it
reads UTF-16 records the way the shim and setup.mjs now do.
- index.mjs: serves GET /dsh-plugin-rea/guide-state, mounted only through
ctx.inject(['webServer']), so headless and ACP hosts load the plugin exactly
as before. Read-only: no provisioning, no spawning, no writes.
- client.js: shell.overlay card shown once per plugin version, with the exact
command for this install; "Later" and "I ran it" record the version in
localStorage, so the next upgrade re-arms it and the same version stays quiet.
- cordis.patch.yml: one extra row for the package entry only (no tools, no REA).
- 5 new tests (78 total); verify-branch section 9 checks all four ends of the
chain, so a rename cannot make the guide silently never appear.
systemReaCandidates followed this process's platform instead of the one it is resolving, so the win32 shape could never be exercised on POSIX; a failed background spawn left a lock naming a live pid (and an unhandled error event could kill the shim), so it now clears or marks the lock failed and logs why; probeRea accumulated stdout without a bound; readJson now decodes UTF-16 LE/BE in both the shim and setup, because PowerShell 5.1 writes that by default; foreignReaRegistration strips a YAML trailing comment.
A market install or update replaces the package and stops; nobody runs setup.mjs for the user. That sentence now sits at the top of both READMEs, and the standing per-release note in the changelog says the same so later release notes inherit it.
check-updates compared every component by major version, so 6.5.0 matched the 6.7.0 pin and the only signal that setup must be re-run disappeared. Dotted pins now compare exactly; a major-only pin (jdk 21) still matches its family; an adopted (reused) Node is checked against engines and is never counted as pin-consistent.
Also: the screenshot generator reads versions from the manifest instead of hardcoding 6.5.0, and both READMEs state the two standing upgrade rules (the mirrored launcher self-heals from 2.0.8; the mirrored setup copy is refreshed only by running setup from the plugin directory).
6.6.0 was tagged upstream but never published to npm; 6.7.0 is npm latest and keeps
the contract unchanged: same engines (^22.19.0 || ^24.11.0 || >=26.0.0), same bin
(scripts/rea.mjs), same optional frida 17.23.2, and the tarball still ships no
node_modules (1775 entries, 0 there), so provisioning stays an npm install.
Measured on this machine: rea --version 6.7.0, MCP initialize answers
serverInfo {name: rea, version: 6.7.0}, tools/list and doctor both report 207 tools
- unchanged from 6.5.0, so this is a feature/fix upgrade rather than tool growth.
doctor gains kuna-installation, another caller-supplied check provisioning leaves alone.
The rea stub in the tests now takes its version from the manifest pin instead of
hard-coding it: a bump otherwise fails --offline for a reason unrelated to offline.
2.0.9 taught the shim to drop a leading U+FEFF but left setup.mjs parsing raw.
Both programs read the same mirror.json and bundle.json, so a BOM made one file
mean different things: the shim found the checkout, setup did not — and a
mirrored setup re-mirrored the previous manifest and provisioned from it.
bundle.json was read as "nothing is provisioned" for the same reason.
Both sides now use the same readJson(). verify-branch.ps1 and CI fail on a bare
JSON.parse(readFileSync(...)) in either file, and two tests cover a BOM-ed
mirror.json and a BOM-ed bundle.json; both fail without the strip (checked).
Each item was reproduced before it was changed, and every fix has a test that
fails when it is reverted (18 reverse checks; two of them on WSL because the
code path only exists on POSIX).
- A lock older than the freshness window was never removed, so the exclusive
create below it failed with EEXIST forever: after a killed download the shim
never provisioned again — and said nothing, because main() printed no message
for "in-flight". Reproduced with a 7-hour dead lock (no run started, lock
untouched, no output) against a 5-minute control lock that behaved correctly.
An expired lock, or one whose holder is gone, is now cleared; "in-flight",
"spawn-failed" and "unwritable" each say what they are.
- examples/ still held the 1.x rows (`command: rea`, `cmd.exe /c rea`) and shipped
inside the 2.x npm package, where copying one bypasses the bundle row entirely.
Removed from main (v1 keeps them, where they are correct) together with the
package.json files[] entry and the three validate.yml checks that named them;
the payload is 19 files now.
- `--offline` was a promise the npm step ignored: provisionReaAgents ran
`npm install` regardless. It now refuses that step when the vendored copy is
unusable, and still accepts one that is.
- That npm step is the only one that cannot use this bundle's own route/proxy
resolution; its failure now says so, and both READMEs name HTTPS_PROXY /
NPM_CONFIG_REGISTRY.
- POSIX extraction had exactly one tool per format while Ghidra is a zip on every
platform, so a bare image without unzip failed for certain. zip now falls back
unzip -> python3 -m zipfile -> the provisioned JDK's jar, tarballs tar ->
python3, and when none is present the log names the tools it tried.
- The platform-tools arm64 comments were wrong (Google ships x86_64 binaries for
both linux and darwin): corrected, so an arm64 host gets an accurate
optional-component report instead of an implied native adb.
- docs/bundle-design.md caught up with the implementation: the row resolves
<DSH_HOME>/plugins/rea-bundle/bin/rea-shim.mjs at load time, and the vendored
CLI lives under vendor/node_modules/rea-agents.
- release.yml's publish job now requires the version's tag and its release asset,
so a workflow_dispatch run cannot publish to npm while the market's fallback
tarball link is missing.
- Numeric environment overrides are sanitised instead of becoming NaN ("gave up
after NaN attempt(s)"), and the effective limits are logged; the PowerShell
route's docstring now says it is the primary Windows route; five redundant
dynamic `node:fs` imports are gone.
37 unit tests (Windows 35 pass + 2 platform skips; WSL 36 pass + 1), audit
clean, verify-branch ALL CHECKS PASSED, and the real machine re-provisioned with
the mirror byte-identical to this tree (REA 6.5.0, 207 tools, a live tool call
reporting path_source: environment).
Every item below was confirmed in the code (or reproduced) before it was changed,
and each now has a regression test that fails when its fix is reverted.
- `--only` rewrote bundle.json from this run's results alone, erasing every other
component's record: the shim stopped exporting JAVA_HOME/GHIDRA_INSTALL_DIR and
the next full run re-extracted them. Untouched records are carried over, and
only while their recorded path still exists.
- `--only` skipped the requires check entirely, so `--only apktool` resolved
{jvm} to a vendor/jdk that only a full run creates and failed on a machine whose
JDK was recorded next to it.
- A complete .part was not treated as finished work: it logged "already complete"
and then asked for `Range: bytes=<size>-` (416, four attempts, reported failed).
It is verified and renamed now.
- The Windows PowerShell route deleted the partial file before every attempt, and
the Node fallback reused a stale `have`, appending a full 200 response onto the
bytes a failed route had already written. The route resumes from what is on
disk, and the fallback re-reads the file's real length.
- POSIX wrappers landed 0644 while REA spawns them without a shell (EACCES); a
POSIX entry marked `executable` is now set 0755.
- apktool's wrapper was pinned to `master` with a hard checksum, so one upstream
commit was enough to break every new install. Pinned to the immutable v3.0.3
tag, with the hashes measured from it.
- `--reuse` adopted a JDK because `javac` existed and a Ghidra because
`analyzeHeadless` existed. A candidate must now pass the component's own probe,
and a `file` probe finally honours `expect.contains`.
- A malformed bundle.json failed permanently: setup could not get far enough to
replace the file it could not parse, and nothing started setup. Setup tolerates
it, and the shim provisions for `malformed` as well as `missing`.
- A failed run kept a fresh lock that made the shim claim "provisioning started in
the background" for six hours while nothing ran. The lock records its state and
pid, and a dead holder no longer blocks a retry.
- A successful `--json` run never released the lock, because the removal sat
inside the non-`--json` branch.
- The shim's fallback REA path omitted `node_modules`, so an older bundle.json
without paths.reaEntry reported a working install as gone.
- bootstrap.ps1 threw on a non-numeric `node -v` (an int cast under
ErrorActionPreference=Stop) instead of falling through to the portable download,
and could exit 0 when `&` failed; it reads REA's engines from the manifest pin
instead of a second hardcoded copy, and -DryRun no longer creates directories
while claiming nothing changed.
- check-updates.ps1 looked for REA in the npm global root and for Ghidra in a
user-level variable, which is the 1.x story: on a 2.x machine the REA section
pointed at the wrong object, the doctor self-check was skipped entirely, and the
bundle drift check silently skipped unrecorded pins while printing "all pins
match". It reads bundle.json first now.
audit-manifest.mjs gained four checks: manifest components vs INSTALL_ORDER,
pins.nodeEngines vs package.json engines, the REA pin probe against the next
patch/minor, and the POSIX wrapper exec flags. validate.yml gained a
windows-latest job, because the PowerShell download route, the zip extraction and
the bootstrap were Windows-only code that no CI ever ran.
33 unit tests (32 pass, 1 platform skip), audit clean, on Windows and WSL.
1.x was only mentioned in the opening version note and the upgrade section; the question
"when would I actually want it?" had no answer in the README. Both READMEs now end (just
before the license) with a short section: what 1.x is — the adapter without provisioning,
launching `rea mcp` from PATH — when it is the better choice (you already manage REA and
Ghidra, you do not want the plugin fetching anything, or the row must point at your own
`rea`), and how to install it (`[email protected]` or `github:czcdr/dsh-plugin-rea#v1.0.6`).
Docs-only patch release so the npm package page renders it.
The npm package page renders the README straight from the published tarball, and its first
screen was title -> badge -> language line -> a 1280x720 screenshot, which pushed the 1.x/2.x
explanation below the fold — the 2.0.0 README therefore looked unchanged there.
- both READMEs: the version note (which line is this, what 1.x vs 2.x do, where 1.x lives)
now sits directly under the language switcher; the comparison table and the upgrade
section keep their places
- docs/bundle-design.md: main IS the bundle line now (it said "this branch, main unchanged"),
and section 8 is about the approach, not a branch
- docs-only patch release on purpose: republishing is the only way npm re-renders that page
main fast-forwards onto the bundle branch (no merge commit), so the default branch,
npm latest and the market all point at the all-in-one variant; the 1.x
configuration-only adapter is preserved on the new v1 branch and on npm 1.0.6.
- package.json 2.0.0-bundle.2 -> 2.0.0; CHANGELOG gains the 2.0.0 entry including the
1.x -> 2.x upgrade steps (run setup once, then restart: the 2.x row launches a shim
that only setup creates, so an unprovisioned upgrade loses the tools silently)
- both READMEs: a new "two lines" section comparing 1.x and 2.x, install commands
switched to the npm name (plus a #v2.0.0 pin), an "Upgrading from 1.x" section, and
a Verified-against note that separates what was exercised live from what was not
(the JDK + Ghidra download branch, macOS/Linux first runs)
- assets/make-screenshots.ps1: the diagram now shows the 2.x chain (host Node ->
mirrored shim -> vendored REA -> engines), and its param default no longer reads
$PSScriptRoot, which is not populated while parameter defaults are bound under
Windows PowerShell 5.1 — the documented invocation died there, so this script had
never actually regenerated a screenshot. Both screenshots regenerated.
- docs/marketplace-entry.yml: 2.x description, and the stale "current v1.0.5" comment
Two contracts this repository already states but the code did not keep, both
found by running the bundle on a real machine:
- manifest.components.androidPlatformTools declares optional: true (without a
device these tools simply report no devices), but setup.mjs never read the
field, so an unreachable dl.google.com - the only Google-hosted component -
failed the whole run on a machine where everything REA needs had provisioned
fine. An optional component's failure is now reported as such, kept out of the
exit code, and left unrecorded so the next run retries it; a component that
another component requires is never treated as optional.
- manifest.components.node.probe says the reuse path must reject an incompatible
runtime, but adoption was fileExists-only: a machine whose PATH Node is 20
recorded it in bundle.json, and the failure surfaced much later inside REA.
Every candidate (REA_BUNDLE_NODE and each PATH hit, not just the first) now has
to pass that probe; one that cannot even be run is rejected with its reason.
README: apktool's real Windows gate (upstream execFile's the launcher without a
shell, so the official .bat cannot work) and the fact that the CLI refuses to
manage the Electron app's desktop profile.
Tests: 21 (was 17); Windows 21/21 and WSL 21/21. Reverse-verified - disabling the
optional accounting reddens that test, disabling the reuse check reddens the
other two.
The clean-machine entry point threw AFTER a successful download, checksum and
extraction: \ stripped the extension twice, so it looked for
<stack>\node-v24.21\node.exe while the official zip unpacks to
node-v24.21.0-win-x64. Reachable only where no Node is on PATH — the entire
audience of this script — which is why unit tests, the audit, WSL and the
sandboxed end-to-end all missed it. Reproduced on a real machine by stripping
PATH down to the OS directories.
- strip the extension once, and accept any directory that actually carries
node.exe: a naming drift must not strand a machine that already paid for the
download and verified its checksum
- -DryRun now also recognises an already-unpacked portable Node (it looks,
it never fetches) so the plan tells the truth about whether a download is
needed
- verify-branch.ps1 section 8: with an empty PATH and the official layout, the
bootstrap must find the portable Node and must plan no download — a
zero-download regression guard, reverse-verified against HEAD (2 CHECK(S)
FAILED, 'plan: download and extract ...' on the stack)
`--dry-run` reported the whole chain as broken on a clean machine. Every planned
component returns `{ planned: true }` with no path, and only a real path was
recorded in `context.resolved`, so each dependent plan entry was reported as
"required component ... is unavailable" — four FAIL lines and exit 1, for exactly
the reader the README tells to run `--dry-run` first. Adoption on a developer
machine hid it: with `--reuse` everything resolves and the identical command
exits 0.
Also:
* verify-branch.ps1 asserted "dry-run exits 0" through `--reuse`, so it passed
precisely where the defect could not appear and would have failed on a bare
machine. It now runs without `--reuse`, asserts that no dependency is reported
unavailable, and checks its own BOM (it is the script edited most often).
* release.yml pinned its npm self-update to `npm@^11` rather than `latest`, so a
publishing pipeline cannot acquire a new npm major on release day.
* check-updates.ps1's engines verdict answered "unsatisfied" for patterns it does
not implement (`22.x`, `>=22`), sending users to upgrade a Node that is fine. It
now answers "cannot be parsed". Verified through Windows PowerShell 5.1 against
fake REA packages: 22.x → cannot parse, >=99.0.0 → unsatisfied, ^24.11.0 → ok,
and removing the guard makes 22.x unsatisfied again.
* the audit compares the two READMEs' section and subsection counts, because the
bilingual pair is maintained by hand and drifts the same way the patch did.
Verified: audit clean with 6/6 deliberate sabotages caught; 17/17 unit tests on
Windows and on Linux; `--dry-run` exits 0 both with and without `--reuse`; 25/25
end-to-end path checks; verify-branch ALL CHECKS PASSED; and the row → shim → MCP
handshake still returns rea 6.4.0 with 170 tools.
package.json exposes bin/rea-shim.mjs as the `rea-bundle-status` bin, and npm's
bin shim can only forward the arguments a user typed — it cannot append the
`--shim-status` the name implies. So the one command that most invites a bare run
fell through to the launch path: it started REA, and on an unprovisioned machine
it began an ~800 MB download into the bundle root.
An independent review found this while verifying the previous commit, and it also
correctly noted that the previous CHANGELOG overstated its fix: only the flagged
form (`rea-bundle-status --shim-status`) was corrected there. The claim is now
accurate, and the bare form is fixed.
A bare invocation prints usage and exits 0. The MCP row is unaffected because it
always passes `mcp` — confirmed by the handshake check (rea 6.4.0, 170 tools).
Pinned by a test that guards itself: it replaces the plugin's setup.mjs with a
stub, so even if this guard is ever removed the test reaches a stub rather than a
download. Deleting the guard turns that test red, which is how it was verified;
the sabotage run created no files and left no processes behind.
`!!js` cannot reconcile a machine with no DSH_HOME, USERPROFILE and HOME: the row
resolves to the filesystem root, while setup.mjs would go through os.homedir()
and land on the passwd entry. The behaviour is harmless — the row reports
MODULE_NOT_FOUND, writes nothing and downloads nothing, and the harness carries
on — so it is now pinned as a warning rather than left uncovered. An independent
review noticed the reason it had no coverage: all three environments the audit
evaluated kept either USERPROFILE or HOME, so nothing reached this case.
Also records that package.json's `rea-bundle-status` bin entry points at the same
checkout copy of the shim, which the previous commit therefore fixes as well —
verified against an installed layout (node_modules/dsh-plugin-rea with its
cordis.patch.yml): the reported bundle root matches <DSH_HOME>/plugins/rea-bundle.
The previous round made the shim resolve its bundle from its own location, so a
custom DSH_HOME could not send it looking in ~/.dsh. That is right for the
mirrored copy the published row launches — but wrong for the checkout copy a
human runs, whose own directory is the plugin, not a bundle:
* `npm run bundle:status` reported "not provisioned" on a fully provisioned
machine, and the status line setup prints pointed at that same copy;
* a bare `node bin/rea-shim.mjs mcp` would have provisioned ~800 MB into the
plugin directory.
Only a checkout ships cordis.patch.yml, so that file now discriminates: the
mirrored copy keeps using its own position (an MCP child has no DSH_HOME to
consult), while the checkout resolves the same root setup.mjs writes — including
a custom --root, which is why the status hint now names the mirrored copy.
The audit guards the discriminator by rejecting a mirror list that would copy it
along; mirroring it would silently flip a mirrored copy back to the DSH_HOME
rule.
The README's "install the plugin first" paragraph was also false: the row points
at a file setup creates, so before the first provisioning run it exits with
MODULE_NOT_FOUND and nothing starts a background provisioning. It now says so and
names the one manual step.
Verified: 16/16 unit tests on Windows and on Linux — three of them spawn the shim
in both layouts, and reverting the fix turns the checkout one red; audit clean
with 5/5 deliberate sabotages caught; 25/25 end-to-end path checks; and the row →
shim → MCP handshake still returning rea 6.4.0 with 170 tools.
The design document still showed the row launching `<plugin>/bin/rea-shim.mjs`,
but the patch points at the mirrored copy under the bundle root, and the shim
resolves its bundle from its own location rather than DSH_HOME — every DSH_* name
is stripped from an MCP child's environment, so the old description hid exactly
the failure mode that mattered.
It also now explains the two constraints this round established: a mirrored
`setup.mjs` reads `<bundle>/manifest/bundle.manifest.json`, so the manifest
travels with the copy, and `<bundle>/mirror.json` has to name the checkout or a
later mirrored run can only copy the bundle onto itself. The quoting rule for
`!!js` scalars containing a ternary is recorded next to the other design
constraints, since YAML reads the bare form as a key/value pair and the loader
then receives a mapping instead of an expression.
Three defects made the published flow impossible to complete:
* The !!js scalar in the MCP row was unquoted, and a bare scalar containing
"? " and ": " is read by YAML as an explicit key/value pair. The loader
therefore received a mapping, so args[0] was {"[object Object]": "..."} and
the row could never start. Reproduced against the harness's own dialect
(JSON_SCHEMA plus the !!js type definition read out of app.asar).
* The DSH_HOME fallback stopped one level above <home>/.dsh, while the shim,
setup.mjs and bootstrap.ps1 all resolve to <home>/.dsh. DSH never sets
DSH_HOME — zero writes anywhere in the archive — so this was the default
path, not an edge case: the row pointed at a file provisioning never writes.
* The mirror step copied the shim and setup.mjs but not the manifest, and the
mirrored setup derives its plugin root from its own location. Background
provisioning therefore died on a bare ENOENT that stdio:"ignore" hid, while
the lock it had written suppressed retries for six hours.
Also fixed:
* The shim locates its bundle from its own position. MCP children have every
DSH_* name stripped (case-insensitively), so with a custom DSH_HOME the old
shim looked in ~/.dsh, reported "not provisioned" and started a second
~800 MB run there.
* setup records the real checkout in <bundle>/mirror.json, so a mirrored or
later-upgraded install finds the source instead of copying the bundle onto
itself and provisioning the previous manifest forever.
* Empty REA_BUNDLE_ROOT / DSH_HOME no longer degrade the root to "" or a
relative path, and --root/--cache are resolved (a relative root would be
recorded in bundle.json and make every component look deleted).
* fail() no longer recommends a mirrored copy that cannot work, and
check-updates.ps1 reports the migration state for old bundles.
Guarded by checks that were themselves reverse-tested — 4/4 deliberate
sabotages caught: unquoted !!js mapping detection, DSH_HOME agreement across
unset/empty/set, README-versus-patch drift, and mirror-list completeness. CI
now runs bundle:test and bundle:audit, and its YAML step registers the !!js
scalar constructor: safe_load had been rejecting the very row this repository
ships, so the workflow was permanently red.
Verified: audit clean; 13/13 unit tests on Windows and on Linux (WSL); 25/25
end-to-end path checks in a sandbox; and a full JSON-RPC handshake launched
from the row's own evaluated command line (rea 6.4.0, 170 tools) with DSH_HOME
absent from the child environment.