fix(runtime)!: require patched Node.js security releases (#6315)

Require Node.js ^22.23.4 || ^24.22.1 || >=26.12.1 across package engines, setup/doctor and the standalone installer. Validate patch boundaries and maintain runtime-independent listener identity, heap-option and HTTP schema-binding tests.

Closes #6283
Refs #6480

BREAKING CHANGE: Node versions below 22.23.4, 24.22.1 and 26.12.1 are no longer supported. Upgrade Node before installing or updating REA; the installer does not upgrade Node.

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
ShikamaruNaaraa
2026-10-23 12:51:50 +05:30
committed by GitHub
parent 49c9c9d4a1
commit 28a33cae8f
19 changed files with 142 additions and 108 deletions
+1 -1
View File
@@ -933,7 +933,7 @@ jobs:
tests/boundary/filesystem/javascriptArtifactContentIdentity.test.ts
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22.19.0
node-version: 22.23.4
- name: Verify native path boundaries on Node 22
run: >-
npx vitest run --coverage.enabled=false
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
# Exercise unavailable-runtime diagnostics, the first supported release,
# and new patches on both supported lines without hand-editing versions.
# The repository-pinned Node 24 version also runs the complete test suite.
node: ["22.x", "24.15.0", "24.x", "26.x"]
node: ["22.x", "24.22.1", "24.x", "26.x"]
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+1 -1
View File
@@ -1 +1 @@
24.18.0
24.22.1
+1 -1
View File
@@ -177,7 +177,7 @@ CLI commands use the same workflows.
## What you can analyze
REA requires Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+, plus npm.
REA requires Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+, plus npm.
Additional tools and host support depend on the target:
| Target | What REA returns | Requirements and guide |
+1 -1
View File
@@ -1369,7 +1369,7 @@ equivalent entry point. Setup run through `npx` registers
`cmd.exe /d /c` so clients can start the runner's command shim. Neither
registration points into the runner's temporary package cache.
REA supports Node.js 22.x (>=22.19), 24.x (>=24.11), and 26+. Node.js 23, 25, and prereleases are unsupported. It uses the npm already paired with that runtime and never upgrades Node.js, npm, or Homebrew.
REA supports Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), and 26.12.1+. Node.js 23, 25, earlier 26.x releases, and prereleases are unsupported. It uses the npm already paired with that runtime and never upgrades Node.js, npm, or Homebrew.
When REA runs on Bun, the `rea doctor` `node` check reports the Bun version and
the Node.js compatibility version Bun declares, and passes when that
+7 -7
View File
@@ -58,7 +58,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
45.5,src/application/Uninstall.ts:removeClient restore,Configuration update fails and restoring the original also fails,"Configuration could not be updated or restored. Restore the backup at {backupPath} manually, then rerun uninstall.","High risk; persistent configuration may be damaged, but retained backup is authoritative",Keep new recovery copy,implemented,verified by injected restoration failure test
46,src/application/Uninstall.ts:removeManagedPath symlink,Managed cache/state/skill path is a symbolic link,Refused to follow symlink {path}.,Exposes local path and does not explain manual safety check,"REA did not remove this item because its managed path is a symbolic link. Verify the link target before removing it manually.",implemented,focused assertion passed
47,src/application/Uninstall.ts:removeManagedPath failure,Managed item removal fails,{path} could not be removed.,Exposes local path and gives no recovery,"This item could not be removed. Check file permissions, then rerun uninstall.",implemented,verified by injected managed-path removal failure test
48,src/application/DoctorDiagnostics.ts:node,Runtime version is outside supported ranges,"Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",Low risk; exact dependency recovery,Keep current copy,unchanged,verified by doctor unit test
48,src/application/DoctorDiagnostics.ts:node,Runtime version is outside supported ranges,"Install Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+.",Low risk; exact dependency recovery,Keep current copy,unchanged,verified by doctor unit test
49,src/application/DoctorDiagnostics.ts:host,Host OS/distribution is unsupported,"REA supports macOS 12+, Linux distributions with a recognized apt, dnf, or pacman package family, and the experimental Windows x64 Ghidra P0 boundary; automated Hopper setup additionally requires Ubuntu 24.04+, Fedora 41+, Nobara 44+, 64-bit Arch Linux, CachyOS, or Omarchy.",Low risk; clearly states supported choices,Keep current copy,unchanged,verified by doctor unit tests
50,src/application/Doctor.ts:analysis engine,Optional deep-analysis executable cannot be found,"Run rea setup to install Hopper, or set HOPPER_LAUNCHER_PATH.",Low risk in setup-specific diagnostic; exact recovery paths,Keep current copy,unchanged,verified by doctor unit tests
51,src/application/Doctor.ts:target,Optional target path fails shared validation,Supply a readable local app or program path.,Low risk; tells caller valid target class,Keep current copy,unchanged,verified by doctor unit test
@@ -68,7 +68,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
55,src/application/Setup.ts:optional engine,Setup completes without optional deep-analysis engine on a host where automated Hopper installation is supported,Hopper is optional for non-Hopper providers. Rerun with --yes --install-hopper for deep native analysis.,Exposes provider terminology but this is an explanatory setup status rather than an error; recovery command is exact,Keep current copy,unchanged,verified by setup unit test
56,src/application/Setup.ts:doctor fallback,Post-setup diagnostics remain unhealthy (including hosts where automated Hopper installation is unsupported),Run rea doctor and apply each reported remediation.,Low risk; delegates to structured per-check recovery,Keep current copy,unchanged,verified by exact setup unit test
57,src/application/SetupPlan.ts:unsupported platform,Platform is neither macOS nor Linux,REA supports Hopper on macOS and selected x86_64 Linux distributions.,Low risk; states support boundary,Keep current copy,unchanged,verified by setup unit test
58,src/application/SetupHost.ts:unsupported Node,Runtime version cannot run setup,"Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ and rerun setup.",Low risk; exact dependency and retry,Keep current copy,unchanged,verified by setup host tests
58,src/application/SetupHost.ts:unsupported Node,Runtime version cannot run setup,"Install Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+ and rerun setup.",Low risk; exact dependency and retry,Keep current copy,unchanged,verified by setup host tests
59,src/application/SetupPlan.ts:unsupported macOS,"macOS version is missing, unparseable, or older than 12",Upgrade to macOS 12 or newer.,Low risk; exact support floor,Keep current copy,unchanged,verified by setup unit test
60,src/application/SetupPlan.ts:unsupported Linux,Linux distribution is outside supported set,"Automated Hopper setup requires an x86_64 host running Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS, or Omarchy; configure an existing supported provider instead.",Low risk; exact supported alternatives,Keep current copy,unchanged,verified by setup unit test
61,src/application/process/ProcessCli.ts:readJson read,Process scenario or evidence file cannot be read,Raw filesystem exception through CLI framework,Can expose local paths/codes and no recovery,"Process input file could not be read. Check that the path exists and is readable.",implemented,verified by focused CLI test
@@ -86,20 +86,20 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
74,install.sh:unknown option,Installer receives an unsupported flag,unknown option with caller-provided flag,Low risk; identifies offending input,Keep current copy,unchanged,verified by shell branch inspection
75,install.sh:platform,Installer runs outside macOS or supported Linux,REA supports macOS and selected 64-bit Linux distributions.,Low risk; states support boundary,Keep current copy,unchanged,verified by shell branch inspection
76,install.sh:curl missing,curl is absent,curl is required.,No recovery instruction,"curl is required. Install curl, then rerun this installer.",implemented,verified by exact missing-curl installer test
77,install.sh:node missing,node is absent,"Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ is required; install it from nodejs.org and retry.",Low risk; exact versions and recovery,Keep current copy,unchanged,verified by shell branch inspection
77,install.sh:node missing,node is absent,"Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+ is required; install it from nodejs.org and retry.",Low risk; exact versions and recovery,Keep current copy,unchanged,verified by shell branch inspection
78,install.sh:npm missing,npm is absent,npm is required; install it with Node.js and retry.,Low risk; exact recovery,Keep current copy,unchanged,verified by shell branch inspection
79,install.sh:node unreadable,node exists but does not report a parseable version,could not determine the active Node.js version.,No cause-local recovery,"The active Node.js version could not be read. Check that node works and is on PATH, then retry.",implemented,verified by exact unreadable-Node-version installer test
80,install.sh:Node 22 floor,Node 22 minor version is below 19,"Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",No install guidance; operator must guess between nvm/fnm/system packages/nodejs.org,"Node.js VERSION is unsupported.
80,install.sh:Node 22 floor,Node 22 version is below 22.23.4,"Node.js VERSION is unsupported; use Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+.",No install guidance; operator must guess between nvm/fnm/system packages/nodejs.org,"Node.js VERSION is unsupported.
REA requires Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Install one via:
REA requires Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+. Install one via:
nvm: nvm install 24 && nvm use 24 (https://github.com/nvm-sh/nvm)
fnm: fnm install 24 && fnm use 24 (https://github.com/Schniz/fnm)
Direct download for OS/ARCH:
https://nodejs.org/dist/latest-v24.x/ (grab the OS-ARCH tarball)
Then rerun this installer.",implemented,verified by exact installer test using Node 20 against the `linux/x64` fake host and by shell branch inspection
81,install.sh:Node 24 floor,Node 24 minor version is below 11,"Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",No install guidance; operator must guess between nvm/fnm/system packages/nodejs.org,"Node.js VERSION is unsupported.
81,install.sh:Node 24 floor,Node 24 version is below 24.22.1,"Node.js VERSION is unsupported; use Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+.",No install guidance; operator must guess between nvm/fnm/system packages/nodejs.org,"Node.js VERSION is unsupported.
REA requires Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Install one via:
REA requires Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+. Install one via:
nvm: nvm install 24 && nvm use 24 (https://github.com/nvm-sh/nvm)
fnm: fnm install 24 && fnm use 24 (https://github.com/Schniz/fnm)
Direct download for OS/ARCH:
1 harm_rank location trigger current_copy user_risk proposed_replacement implementation_status verification_result
58 45.5 src/application/Uninstall.ts:removeClient restore Configuration update fails and restoring the original also fails Configuration could not be updated or restored. Restore the backup at {backupPath} manually, then rerun uninstall. High risk; persistent configuration may be damaged, but retained backup is authoritative Keep new recovery copy implemented verified by injected restoration failure test
59 46 src/application/Uninstall.ts:removeManagedPath symlink Managed cache/state/skill path is a symbolic link Refused to follow symlink {path}. Exposes local path and does not explain manual safety check REA did not remove this item because its managed path is a symbolic link. Verify the link target before removing it manually. implemented focused assertion passed
60 47 src/application/Uninstall.ts:removeManagedPath failure Managed item removal fails {path} could not be removed. Exposes local path and gives no recovery This item could not be removed. Check file permissions, then rerun uninstall. implemented verified by injected managed-path removal failure test
61 48 src/application/DoctorDiagnostics.ts:node Runtime version is outside supported ranges Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Install Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+. Low risk; exact dependency recovery Keep current copy unchanged verified by doctor unit test
62 49 src/application/DoctorDiagnostics.ts:host Host OS/distribution is unsupported REA supports macOS 12+, Linux distributions with a recognized apt, dnf, or pacman package family, and the experimental Windows x64 Ghidra P0 boundary; automated Hopper setup additionally requires Ubuntu 24.04+, Fedora 41+, Nobara 44+, 64-bit Arch Linux, CachyOS, or Omarchy. Low risk; clearly states supported choices Keep current copy unchanged verified by doctor unit tests
63 50 src/application/Doctor.ts:analysis engine Optional deep-analysis executable cannot be found Run rea setup to install Hopper, or set HOPPER_LAUNCHER_PATH. Low risk in setup-specific diagnostic; exact recovery paths Keep current copy unchanged verified by doctor unit tests
64 51 src/application/Doctor.ts:target Optional target path fails shared validation Supply a readable local app or program path. Low risk; tells caller valid target class Keep current copy unchanged verified by doctor unit test
68 55 src/application/Setup.ts:optional engine Setup completes without optional deep-analysis engine on a host where automated Hopper installation is supported Hopper is optional for non-Hopper providers. Rerun with --yes --install-hopper for deep native analysis. Exposes provider terminology but this is an explanatory setup status rather than an error; recovery command is exact Keep current copy unchanged verified by setup unit test
69 56 src/application/Setup.ts:doctor fallback Post-setup diagnostics remain unhealthy (including hosts where automated Hopper installation is unsupported) Run rea doctor and apply each reported remediation. Low risk; delegates to structured per-check recovery Keep current copy unchanged verified by exact setup unit test
70 57 src/application/SetupPlan.ts:unsupported platform Platform is neither macOS nor Linux REA supports Hopper on macOS and selected x86_64 Linux distributions. Low risk; states support boundary Keep current copy unchanged verified by setup unit test
71 58 src/application/SetupHost.ts:unsupported Node Runtime version cannot run setup Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ and rerun setup. Install Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+ and rerun setup. Low risk; exact dependency and retry Keep current copy unchanged verified by setup host tests
72 59 src/application/SetupPlan.ts:unsupported macOS macOS version is missing, unparseable, or older than 12 Upgrade to macOS 12 or newer. Low risk; exact support floor Keep current copy unchanged verified by setup unit test
73 60 src/application/SetupPlan.ts:unsupported Linux Linux distribution is outside supported set Automated Hopper setup requires an x86_64 host running Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS, or Omarchy; configure an existing supported provider instead. Low risk; exact supported alternatives Keep current copy unchanged verified by setup unit test
74 61 src/application/process/ProcessCli.ts:readJson read Process scenario or evidence file cannot be read Raw filesystem exception through CLI framework Can expose local paths/codes and no recovery Process input file could not be read. Check that the path exists and is readable. implemented verified by focused CLI test
86 74 install.sh:unknown option Installer receives an unsupported flag unknown option with caller-provided flag Low risk; identifies offending input Keep current copy unchanged verified by shell branch inspection
87 75 install.sh:platform Installer runs outside macOS or supported Linux REA supports macOS and selected 64-bit Linux distributions. Low risk; states support boundary Keep current copy unchanged verified by shell branch inspection
88 76 install.sh:curl missing curl is absent curl is required. No recovery instruction curl is required. Install curl, then rerun this installer. implemented verified by exact missing-curl installer test
89 77 install.sh:node missing node is absent Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ is required; install it from nodejs.org and retry. Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+ is required; install it from nodejs.org and retry. Low risk; exact versions and recovery Keep current copy unchanged verified by shell branch inspection
90 78 install.sh:npm missing npm is absent npm is required; install it with Node.js and retry. Low risk; exact recovery Keep current copy unchanged verified by shell branch inspection
91 79 install.sh:node unreadable node exists but does not report a parseable version could not determine the active Node.js version. No cause-local recovery The active Node.js version could not be read. Check that node works and is on PATH, then retry. implemented verified by exact unreadable-Node-version installer test
92 80 install.sh:Node 22 floor Node 22 minor version is below 19 Node 22 version is below 22.23.4 Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Node.js VERSION is unsupported; use Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+. No install guidance; operator must guess between nvm/fnm/system packages/nodejs.org Node.js VERSION is unsupported. REA requires Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Install one via: nvm: nvm install 24 && nvm use 24 (https://github.com/nvm-sh/nvm) fnm: fnm install 24 && fnm use 24 (https://github.com/Schniz/fnm) Direct download for OS/ARCH: https://nodejs.org/dist/latest-v24.x/ (grab the OS-ARCH tarball) Then rerun this installer. Node.js VERSION is unsupported. REA requires Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+. Install one via: nvm: nvm install 24 && nvm use 24 (https://github.com/nvm-sh/nvm) fnm: fnm install 24 && fnm use 24 (https://github.com/Schniz/fnm) Direct download for OS/ARCH: https://nodejs.org/dist/latest-v24.x/ (grab the OS-ARCH tarball) Then rerun this installer. implemented verified by exact installer test using Node 20 against the `linux/x64` fake host and by shell branch inspection
93 81 install.sh:Node 24 floor Node 24 minor version is below 11 Node 24 version is below 24.22.1 Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Node.js VERSION is unsupported; use Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+. No install guidance; operator must guess between nvm/fnm/system packages/nodejs.org Node.js VERSION is unsupported. REA requires Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Install one via: nvm: nvm install 24 && nvm use 24 (https://github.com/nvm-sh/nvm) fnm: fnm install 24 && fnm use 24 (https://github.com/Schniz/fnm) Direct download for OS/ARCH: https://nodejs.org/dist/latest-v24.x/ (grab the OS-ARCH tarball) Then rerun this installer. Node.js VERSION is unsupported. REA requires Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+. Install one via: nvm: nvm install 24 && nvm use 24 (https://github.com/nvm-sh/nvm) fnm: fnm install 24 && fnm use 24 (https://github.com/Schniz/fnm) Direct download for OS/ARCH: https://nodejs.org/dist/latest-v24.x/ (grab the OS-ARCH tarball) Then rerun this installer. implemented verified by shell branch inspection
94 82 install.sh:version syntax Explicit version is not exact semantic version version must be an exact semantic version. Low risk; exact correction Keep current copy unchanged verified by shell branch inspection
95 83 install.sh:release lookup Latest GitHub release request fails could not resolve the latest GitHub release. No fallback despite supporting pinned versions The latest REA release could not be resolved. Check network access or pass --version VERSION, then retry. implemented verified by exact release-lookup installer test
96 84 install.sh:release response Latest release response is not valid expected JSON Node JSON parser stack followed by the safe release-response message Leaks a parser stack and buries recovery guidance The release response was invalid. Retry later or pass --version VERSION. implemented verified by exact malformed-release test with parser stderr suppressed
97 85 install.sh:release tag Latest release tag does not match REA release convention latest release tag is not a valid rea-agents release. Exposes package naming detail and omits fallback The latest release tag was invalid. Retry later or pass --version VERSION. implemented verified by exact invalid-release-tag installer test
98 86 install.sh:npm prefix npm cannot return its global prefix could not determine the npm global prefix. No recovery instruction The npm global prefix could not be read. Repair the npm configuration, then retry. implemented verified by exact macOS npm-prefix installer test
99 87 install.sh:npm install npm global installation exits nonzero npm package installation failed. No likely causes or recovery npm could not install REA. Check registry access and npm permissions, then retry. implemented verified by exact installer test
100 88 install.sh:binary absent npm exits successfully without executable the rea command was not installed at local path Leaks local path and no recovery npm completed without installing the rea command. Check the npm global bin directory and PATH, then retry. implemented verified by exact absent-installed-command test
101 89 install.sh:version read Installed rea executable cannot report its version could not read the installed REA version. No recovery instruction The installed REA version could not be read. Reinstall the requested version, then retry. implemented verified by exact unreadable-installed-version test
102 90 install.sh:version mismatch Installed rea reports a different version installed version ACTUAL does not match EXPECTED. Low risk; gives exact integrity mismatch Keep current copy unchanged verified by exact installer test
103 90.5 @modelcontextprotocol/server:tool input validation Caller sends arguments that violate a declared tool schema Canonical invalid_request with the SDK diagnostic plus recovery.kind=inspect_input_contract and a bounded instruction to read the advertised schema or call tools/list Keeps field/type correction while telling the agent how to recover; diagnostics over 4 KiB stay on the existing retained-error path Normalize ordinary validation failures and retain oversized diagnostics verified by real in-memory and stdio MCP client/server calls with wrong argument types
104 90.6 incur:unknown CLI command Caller enters an unknown REA command 'COMMAND' is not a command for 'rea'. Suggested command: rea --help Low risk; identifies input and exact discovery action Keep framework-generated copy unchanged verified by real packaged dispatcher invocation
105 91 src/application/binaryDiagnostics/ByteEntropyService.ts Invalid file or window Expected a regular file.; Selected file could not be found.; Selected byte window extends beyond the end of the file.; File size exceeds exact numeric offset representation. Invalid selection cannot produce complete entropy Evidence Choose a readable regular file and a window within its byte size implemented entropy service and MCP boundary tests
+1 -1
View File
@@ -14,7 +14,7 @@ additional permission flag or degraded mode is required.
## Supported boundary
- Windows 10+ x64 with fixed local NTFS target and temporary volumes;
- Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+;
- Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+;
- an operator-installed Ghidra 12.1.x distribution (verified with 12.1.4);
- a 64-bit full JDK inside that installation's declared Java range (JDK 21 or newer, with no maximum, for current 12.1 releases);
- an explicit native, non-managed x86, x86-64, ARMNT (Thumb-2) or ARM64 PE application/DLL, or a
+7 -3
View File
@@ -57,7 +57,7 @@ fail_node_version() {
local observed="$1"
node_host_labels
printf 'REA installation failed: Node.js %s is unsupported.\n\n' "$observed" >&2
printf 'REA requires Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Install one via:\n' >&2
printf 'REA requires Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+. Install one via:\n' >&2
printf ' nvm: nvm install 24 && nvm use 24 (https://github.com/nvm-sh/nvm)\n' >&2
printf ' fnm: fnm install 24 && fnm use 24 (https://github.com/Schniz/fnm)\n' >&2
printf ' Direct download for %s/%s:\n' "$node_os_label" "$node_arch_label" >&2
@@ -112,14 +112,18 @@ main() {
platform="$(uname -s)"
[ "$platform" = "Darwin" ] || [ "$platform" = "Linux" ] || fail "REA supports macOS and selected 64-bit Linux distributions."
command -v curl >/dev/null 2>&1 || fail "curl is required. Install curl, then rerun this installer."
command -v node >/dev/null 2>&1 || fail "Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ is required; install it from https://nodejs.org and retry."
command -v node >/dev/null 2>&1 || fail "Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+ is required; install it from https://nodejs.org and retry."
command -v npm >/dev/null 2>&1 || fail "npm is required; install it with Node.js and retry."
node_version="$(node -p 'process.versions.node' 2>/dev/null || true)"
IFS=. read -r node_major node_minor _node_patch <<<"$node_version"
matches "$node_major" '^[0-9]+$' && matches "$node_minor" '^[0-9]+$' || fail "the active Node.js version could not be read. Check that node works and is on PATH, then retry."
node_patch="${_node_patch%%+*}"
if ! matches "$_node_patch" '^[0-9]+(\+[0-9A-Za-z.-]+)?$' ||
! ((node_major == 22 && node_minor >= 19 || node_major == 24 && node_minor >= 11 || node_major >= 26)); then
! ((node_major == 22 && (node_minor > 23 || node_minor == 23 && node_patch >= 4) ||
node_major == 24 && (node_minor > 22 || node_minor == 22 && node_patch >= 1) ||
node_major == 26 && (node_minor > 12 || node_minor == 12 && node_patch >= 1) ||
node_major > 26)); then
fail_node_version "$node_version"
fi
+1 -1
View File
@@ -84,7 +84,7 @@
"vitest": "5.0.3"
},
"engines": {
"node": "^22.19.0 || ^24.11.0 || >=26.0.0"
"node": "^22.23.4 || ^24.22.1 || >=26.12.1"
},
"optionalDependencies": {
"frida": "17.23.2"
+1 -1
View File
@@ -43,7 +43,7 @@
"rea-agents": "scripts/rea.mjs"
},
"engines": {
"node": "^22.19.0 || ^24.11.0 || >=26.0.0"
"node": "^22.23.4 || ^24.22.1 || >=26.12.1"
},
"packageManager": "npm@11.16.0",
"files": [
+1 -1
View File
@@ -10,7 +10,7 @@ export const createDoctorHostFixture = (
platform: "darwin",
homeDirectory: "/fixture/home",
architecture: "x64",
runtime: { id: "node", version: "24.18.0" },
runtime: { id: "node", version: "24.22.1" },
macosVersion: () => Promise.resolve("14.0"),
linuxDistribution: () => Promise.resolve(undefined),
validTarget: (path) => Promise.resolve(path.includes("Hopper")),
+2 -1
View File
@@ -397,7 +397,8 @@ describe("doctor runtime check", () => {
});
it.each([
{ compatibility: "26.3.0", ok: true },
{ compatibility: "26.12.1", ok: true },
{ compatibility: "26.12.0", ok: false },
{ compatibility: "25.1.0", ok: false },
])(
"identifies Bun and admits it by Node.js compatibility $compatibility",
+1 -1
View File
@@ -32,7 +32,7 @@ export class FakeSetupHost implements SetupHost {
}
registrationCommand: readonly string[];
registrationCommandBlocker?: string;
nodeVersion = "24.18.0";
nodeVersion = "24.22.1";
version: string | undefined = "14.5";
distribution: LinuxDistribution | undefined;
hopper: string | undefined;
+14 -9
View File
@@ -5,20 +5,25 @@ import { javaScriptRuntime, supportsNodeVersion } from "./runtimeVersion.js";
describe("supported Node.js versions", () => {
it.each([
["20.0.0", false],
["22.18.9", false],
["22.19.0", true],
["22.19.0", false],
["22.23.3", false],
["22.23.4", true],
["22.99.0", true],
["23.0.0", false],
["24.10.9", false],
["24.11.0", true],
["24.11.0", false],
["24.22.0", false],
["24.22.1", true],
["24.99.0", true],
["25.1.0", false],
["26.0.0", true],
["26.0.0", false],
["26.12.0", false],
["26.12.1", true],
["26.13.0", true],
["27.0.0", true],
["24.11.0-rc.1", false],
["26.0.0-nightly.20261001", false],
["24.11", false],
["24.11.invalid", false],
["24.22.1-rc.1", false],
["26.12.1-nightly.20261022", false],
["24.22", false],
["24.22.invalid", false],
["invalid", false],
])("classifies %s as supported=%s", (version, expected) => {
expect(supportsNodeVersion(version)).toBe(expected);
+2 -2
View File
@@ -1,11 +1,11 @@
import { satisfies } from "semver";
/** Supported runtime range, kept in sync with the published package engines. */
export const SUPPORTED_NODE_VERSION_RANGE = "^22.19.0 || ^24.11.0 || >=26.0.0";
export const SUPPORTED_NODE_VERSION_RANGE = "^22.23.4 || ^24.22.1 || >=26.12.1";
/** Human-readable form of {@link SUPPORTED_NODE_VERSION_RANGE} for remediation. */
export const SUPPORTED_NODE_VERSION_PROSE =
"Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+";
"Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+";
/** Return whether a Node.js version satisfies REA's tested runtime families. */
export const supportsNodeVersion = (version: string): boolean =>
@@ -2637,7 +2637,7 @@ const originalRemoveListener = EventEmitter.prototype.removeListener;
ipcMain.removeListener = function(channel, callback) {
globalThis.__preHookRemovalCalls.push({
receiverIsEmitter: this === ipcMain,
callbackName: callback.name,
callback,
});
return Reflect.apply(originalRemoveListener, this, arguments);
};`,
@@ -2652,21 +2652,28 @@ const observations = globalThis.__preHookRemovalCalls;
const decorate = (emitter) => {
const native = EventEmitter.prototype.removeListener;
emitter.removeListener = function(channel, callback) {
observations.push({ receiverIsEmitter: this === emitter, callbackName: callback.name });
observations.push({ receiverIsEmitter: this === emitter, callback });
return Reflect.apply(native, this, arguments);
};
return emitter;
};
const scenario = (emitter) => {
const start = observations.length;
const original = function original() {};
emitter.on("ch", original);
emitter.rawListeners("ch")[0].listener = () => {};
emitter.removeListener("ch", original);
return emitter.listenerCount("ch");
return {
remaining: emitter.listenerCount("ch"),
forwarded: observations.slice(start).map(({ receiverIsEmitter, callback }) => ({
receiverIsEmitter, callbackIsOriginal: callback === original,
})),
};
};
const plain = scenario(decorate(new EventEmitter()));
const hooked = scenario(ipcMain);
console.log(JSON.stringify({ plain, hooked, observations,
console.log(JSON.stringify({ plain: plain.remaining, hooked: hooked.remaining,
observations: [...plain.forwarded, ...hooked.forwarded],
hook_error: globalThis.__reaElectronActiveSnapshot().hook_error }));
`,
);
@@ -2677,18 +2684,17 @@ console.log(JSON.stringify({ plain, hooked, observations,
readonly hooked: number;
readonly observations: readonly {
readonly receiverIsEmitter: boolean;
readonly callbackName: string;
readonly callbackIsOriginal: boolean;
}[];
readonly hook_error: boolean;
};
expect(result.hook_error).toBe(false);
expect(result.plain).toBe(0);
expect(result.hooked).toBe(0);
expect(result.observations[0]).toEqual({
receiverIsEmitter: true,
callbackName: "original",
});
expect(result.observations[1]).toEqual(result.observations[0]);
expect(result.observations).toEqual([
{ receiverIsEmitter: true, callbackIsOriginal: true },
{ receiverIsEmitter: true, callbackIsOriginal: true },
]);
});
it("forwards pre-hook on and once decorators exact registrations with an explicit gap", async () => {
@@ -2705,13 +2711,13 @@ const install = (emitter) => {
const nativeOnce = EventEmitter.prototype.once;
emitter.on = function(channel, callback) {
globalThis.__preHookRegistrationCalls.push({
method: "on", receiverIsEmitter: this === emitter, callbackName: callback.name,
method: "on", receiverIsEmitter: this === emitter, callback,
});
return Reflect.apply(nativeOn, this, arguments);
};
emitter.once = function(channel, callback) {
globalThis.__preHookRegistrationCalls.push({
method: "once", receiverIsEmitter: this === emitter, callbackName: callback.name,
method: "once", receiverIsEmitter: this === emitter, callback,
});
return Reflect.apply(nativeOnce, this, arguments);
};
@@ -2729,16 +2735,17 @@ const decorate = (emitter) => {
const nativeOn = EventEmitter.prototype.on;
const nativeOnce = EventEmitter.prototype.once;
emitter.on = function(channel, callback) {
observations.push({ method: "on", receiverIsEmitter: this === emitter, callbackName: callback.name });
observations.push({ method: "on", receiverIsEmitter: this === emitter, callback });
return Reflect.apply(nativeOn, this, arguments);
};
emitter.once = function(channel, callback) {
observations.push({ method: "once", receiverIsEmitter: this === emitter, callbackName: callback.name });
observations.push({ method: "once", receiverIsEmitter: this === emitter, callback });
return Reflect.apply(nativeOnce, this, arguments);
};
return emitter;
};
const scenario = (emitter) => {
const start = observations.length;
const calls = [];
const ordinary = function ordinary() { calls.push("ordinary"); };
const oneShot = function oneShot() { calls.push("oneShot"); };
@@ -2746,12 +2753,20 @@ const scenario = (emitter) => {
emitter.once("ch", oneShot);
emitter.emit("ch");
emitter.removeAllListeners("ch");
return calls;
return {
calls,
forwarded: observations.slice(start).map(({ method, receiverIsEmitter, callback }) => ({
method, receiverIsEmitter,
callbackIdentity: callback === ordinary ? "ordinary" : callback === oneShot ? "oneShot" :
Object.getOwnPropertyDescriptor(callback, "listener")?.value === oneShot ? "onceWrapper" : "unexpected",
})),
};
};
const plain = scenario(decorate(new EventEmitter()));
const hooked = scenario(ipcMain);
const snapshot = globalThis.__reaElectronActiveSnapshot();
console.log(JSON.stringify({ plain, hooked, observations,
console.log(JSON.stringify({ plain: plain.calls, hooked: hooked.calls,
observations: [...plain.forwarded, ...hooked.forwarded],
calls: snapshot.events.filter((event) => event.kind === "main-event-invocation"),
gaps: snapshot.events.filter((event) => event.kind === "ipc-observation-gap"),
hook_error: snapshot.hook_error }));
@@ -2765,7 +2780,7 @@ console.log(JSON.stringify({ plain, hooked, observations,
readonly observations: readonly {
readonly method: string;
readonly receiverIsEmitter: boolean;
readonly callbackName: string;
readonly callbackIdentity: string;
}[];
readonly calls: readonly unknown[];
readonly gaps: readonly {
@@ -2778,19 +2793,19 @@ console.log(JSON.stringify({ plain, hooked, observations,
expect(result.plain).toEqual(["ordinary", "oneShot"]);
expect(result.hooked).toEqual(result.plain);
expect(result.observations).toEqual([
{ method: "on", receiverIsEmitter: true, callbackName: "ordinary" },
{ method: "once", receiverIsEmitter: true, callbackName: "oneShot" },
{ method: "on", receiverIsEmitter: true, callbackIdentity: "ordinary" },
{ method: "once", receiverIsEmitter: true, callbackIdentity: "oneShot" },
{
method: "on",
receiverIsEmitter: true,
callbackName: "bound onceWrapper",
callbackIdentity: "onceWrapper",
},
{ method: "on", receiverIsEmitter: true, callbackName: "ordinary" },
{ method: "once", receiverIsEmitter: true, callbackName: "oneShot" },
{ method: "on", receiverIsEmitter: true, callbackIdentity: "ordinary" },
{ method: "once", receiverIsEmitter: true, callbackIdentity: "oneShot" },
{
method: "on",
receiverIsEmitter: true,
callbackName: "bound onceWrapper",
callbackIdentity: "onceWrapper",
},
]);
expect(result.calls).toEqual([]);
+40 -32
View File
@@ -24,27 +24,15 @@ const execFileAsync = promisify(execFile);
const UNSUPPORTED_NODE_RECOVERY_LINUX_X64 =
"REA installation failed: Node.js 20.0.0 is unsupported.\n" +
"\n" +
"REA requires Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Install one via:\n" +
"REA requires Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+. Install one via:\n" +
" nvm: nvm install 24 && nvm use 24 (https://github.com/nvm-sh/nvm)\n" +
" fnm: fnm install 24 && fnm use 24 (https://github.com/Schniz/fnm)\n" +
" Direct download for linux/x64:\n" +
" https://nodejs.org/dist/latest-v24.x/ (grab the linux-x64 tarball)\n" +
"Then rerun this installer.\n";
describe("curl installer scenarios", { timeout: 20_000 }, () => {
it("installs only a pinned REA CLI with closed stdin", async () => {
const fixture = await createFixture();
const result = await runInstaller(fixture, ["--version", "0.3.0"]);
expect(result.stdout).toContain("REA 0.3.0 is installed");
expect(result.stdout).toContain("Run ");
expect(await readFile(fixture.npmLog, "utf8")).toContain(
"install --global --prefix",
);
expect(await readFile(fixture.reaLog, "utf8")).toBe("--version\n");
expect(await readdir(fixture.temporary)).toEqual([]);
});
it.each(["22.19.0", "24.11.0", "26.0.0"])(
describe("installer Node.js admission", { timeout: 20_000 }, () => {
it.each(["22.23.4", "24.22.1", "26.12.1", "26.13.0", "24.22.1+build.5"])(
"accepts supported Node %s without installing or replacing it",
async (version) => {
const fixture = await createFixture();
@@ -59,22 +47,42 @@ describe("curl installer scenarios", { timeout: 20_000 }, () => {
},
);
it.each(["22.18.9", "24.10.9", "23.0.0", "26.0.0-rc.1"])(
"rejects unsupported Node %s before invoking npm",
async (version) => {
const fixture = await createFixture();
await expect(
runInstaller(fixture, ["--version", "0.3.0"], {
FAKE_NODE_VERSION: version,
}),
).rejects.toMatchObject({
stderr: expect.stringContaining(`Node.js ${version} is unsupported`),
});
await expect(readFile(fixture.npmLog, "utf8")).rejects.toMatchObject({
code: "ENOENT",
});
},
);
it.each([
"22.19.0",
"22.23.3",
"24.11.0",
"24.22.0",
"23.0.0",
"26.0.0",
"26.12.0",
"26.12.1-rc.1",
])("rejects unsupported Node %s before invoking npm", async (version) => {
const fixture = await createFixture();
await expect(
runInstaller(fixture, ["--version", "0.3.0"], {
FAKE_NODE_VERSION: version,
}),
).rejects.toMatchObject({
stderr: expect.stringContaining(`Node.js ${version} is unsupported`),
});
await expect(readFile(fixture.npmLog, "utf8")).rejects.toMatchObject({
code: "ENOENT",
});
});
});
describe("curl installer scenarios", { timeout: 20_000 }, () => {
it("installs only a pinned REA CLI with closed stdin", async () => {
const fixture = await createFixture();
const result = await runInstaller(fixture, ["--version", "0.3.0"]);
expect(result.stdout).toContain("REA 0.3.0 is installed");
expect(result.stdout).toContain("Run ");
expect(await readFile(fixture.npmLog, "utf8")).toContain(
"install --global --prefix",
);
expect(await readFile(fixture.reaLog, "utf8")).toBe("--version\n");
expect(await readdir(fixture.temporary)).toEqual([]);
});
it("resolves and validates the latest REA release tag", async () => {
const fixture = await createFixture();
@@ -551,7 +559,7 @@ const createFixture = async (): Promise<InstallerFixture> => {
executable(
join(bin, "node"),
`#!/bin/sh
if [ "$1" = "-p" ]; then printf '%s\n' "\${FAKE_NODE_VERSION:-24.18.0}"; else exec ${shellQuote(process.execPath)} "$@"; fi
if [ "$1" = "-p" ]; then printf '%s\n' "\${FAKE_NODE_VERSION:-24.22.1}"; else exec ${shellQuote(process.execPath)} "$@"; fi
`,
),
executable(
+21 -15
View File
@@ -2,8 +2,9 @@ import {
Client,
StreamableHTTPClientTransport,
} from "@modelcontextprotocol/client";
import { describe, expect } from "vitest";
import { describe, expect, onTestFinished } from "vitest";
import { advertisedMcpToolSchemas } from "../../fixtures/advertisedMcpTool.js";
import { expectForeignHostRefused } from "../../support/http/rawRequest.js";
import { startMcpHttpProcess } from "../../support/mcp/streamableHttpProcess.js";
import { processTest } from "../../support/process/processFixture.js";
@@ -14,17 +15,26 @@ const HTTP_BOUNDARY_TIMEOUT_MS = 90_000;
const openSession = async (url: URL, name: string) => {
const transport = new StreamableHTTPClientTransport(url);
const client = new Client({ name, version: "1" });
onTestFinished(() => client.close());
await client.connect(transport);
expect(transport.sessionId).toEqual(expect.any(String));
return { transport, client };
const { toolDefinition } = await advertisedMcpToolSchemas(
client,
"binary_session",
);
assertAdvertisedMcpContracts(["binary_session"], [toolDefinition]);
return { transport, client, toolDefinition };
};
/** The shared target answers with its identity only while its providers are still open. */
const expectIdentity = async (client: Client): Promise<void> => {
const identity = await client.callTool({
name: "binary_session",
arguments: {},
});
const expectIdentity = async ({
client,
toolDefinition,
}: Awaited<ReturnType<typeof openSession>>): Promise<void> => {
const identity = await client.callTool(
{ name: "binary_session", arguments: {} },
{ toolDefinition },
);
expect(identity.isError).toBeFalsy();
expect(identity.structuredContent).toMatchObject({
result: {
@@ -45,22 +55,18 @@ describe("rea mcp --http", () => {
stdout += chunk.toString("utf8");
});
const first = await openSession(started.url, "streamable-http-boundary");
const { tools } = await first.client.listTools();
assertAdvertisedMcpContracts(["binary_session"], tools);
await expectIdentity(first.client);
await expectIdentity(first);
await first.transport.terminateSession();
expect(first.transport.sessionId).toBeUndefined();
await first.client.close();
// DELETE ended the session, not the server: the next client's tools still work.
const second = await openSession(started.url, "next-session");
await expectIdentity(second.client);
await expectIdentity(second);
// A client that leaves without DELETE does not block the listener: the next initialize replaces it.
const third = await openSession(started.url, "replacing-session");
expect(third.transport.sessionId).not.toBe(second.transport.sessionId);
await expectIdentity(third.client);
await expect(
second.client.callTool({ name: "binary_session", arguments: {} }),
).rejects.toThrow();
await expectIdentity(third);
await expect(expectIdentity(second)).rejects.toThrow("Session not found");
await second.client.close();
await third.transport.terminateSession();
await third.client.close();
+1 -6
View File
@@ -63,7 +63,7 @@ const probeWorkerHeap = (
env,
encoding: "utf8",
});
expect(result.status).toBe(0);
expect(result.status, result.stderr).toBe(0);
return JSON.parse(result.stdout ?? "") as {
resourceOld: number;
heap: number;
@@ -219,10 +219,6 @@ it("does not report the 128 MiB request for a heap percentage in a Node config f
[`--experimental-config-file=${file}`],
undefined,
);
const split = probeWorkerHeap(
["--experimental-config-file", file],
undefined,
);
const withAbsolute = probeWorkerHeap(
[`--experimental-config-file=${file}`, "--max-old-space-size=256"],
undefined,
@@ -233,7 +229,6 @@ it("does not report the 128 MiB request for a heap percentage in a Node config f
directory,
);
expect(fromFile.resourceOld).toBe(128);
expect(split.resourceOld).toBe(128);
expect(withAbsolute.resourceOld).toBe(128);
expect(fromDefault.resourceOld).toBe(128);
for (const execArgv of [