deps: bump to current upstream, and ship the Wakaru engine REA advertises

Policy note first, because it explains the shape of this commit: pins follow
rea-agents, and nothing is bumped on its own schedule. This round is the explicit
exception - the checker found a real capability gap (REA 6.5.0 advertises
recover_javascript_sources while the image shipped nothing for it, so the
operation reported provider_missing). docs/UPDATES.md now states the rule and
names the only two exceptions: a BEHIND-REA drift, and a MISSING capability.

Wakaru 1.14.0 (REA's JavaScript source-recovery engine):
- Installed from the official release asset, not npm: REA resolves and
  fingerprints the executable itself, and the npm route's node_modules/.bin entry
  is a Node shim it rejects. REA_WAKARU_COMMAND is absolute, as REA requires, and
  the image also carries util-linux prlimit (REA's resource limiter for it).
- The pin is two values on purpose: WAKARU_VERSION and WAKARU_LINUX_X64_SHA256.
  The digest is REA's own audited value for that asset (docs/javascript-recovery.md
  in the pinned release), so the build fails if the bytes ever differ.
- 13 new harness checks. Five assert REA's admission rules on the binary (absolute
  path, readable+executable, the exact "wakaru <semver>" banner, the audited
  digest, prlimit). Eight run the operation: a minified bundle fixture is
  recovered and the Evidence is asserted on schema fields (provider wakaru,
  status complete, failed 0, module decompiled, and the fingerprinted digest
  proving REA ran the audited build), the published module file is checked, and
  the recovered tree is then analysed with analyze-javascript-application.
- The checker gained a digest row comparing our pin with the digest REA documents,
  and its wakaru row is no longer hardcoded as missing.

Bumps applied: Node 24.21.0 to 24.22.0, frida 17.23.2 to 17.23.3, uv 0.13.0 to
0.13.1, supergateway 4.1.0 to 4.2.0, plus Wakaru.

Unicorn stays at 2.1.2, and the build proved why: pip refuses unicorn 2.1.4 with
ResolutionImpossible because pwntools 4.15.0 declares
"unicorn !=2.1.3, !=2.1.4". The constraint is now recorded in the Dockerfile
comment, in the docs, and in a HELD map in the checker, which reports such
components as "held on <pin>: <reason>" instead of as an available update.

Two build defects found and fixed rather than worked around:
- The Node layer failed with "sha256sum: no properly formatted checksum lines
  found": /var/cache/rea-dl is a shared build cache keyed by filename, and it held
  the PREVIOUS version's SHASUMS256.txt under a version-less name, so the new
  tarball's checksum line was never found. Cached filenames are now
  version-qualified (the same hazard was latent for the Wakaru tarball, fixed
  too), and the stale file was deliberately left in place so the fix had to work
  without a cache clean. This is documented as a trap in docs/UPDATES.md.
- Neither failure could reach GHCR: publishing only runs after verification.

Checker semantics now match the policy: exit 1 means the image is wrong NOW
(BEHIND-REA, MISSING, MISMATCH) or a new REA release exists (NEW REA - the
trigger to move everything). Merely-newer upstream releases are listed as waiting
for the next release and do not fail the weekly job, which is the difference
between a useful reminder and a permanently red run.

Verification: full per-engine sweep on all four rebuilt images, 0 failures -
all 120/0/3, ghidra 108/0/8, hopper 101/0/7, ida 93/0/11, exit 0 everywhere.
Full log (kernel string redacted) in docs/verify-engines.txt; image sizes in
docs/IMAGES.md refreshed from make sizes.
This commit is contained in:
Hermes Agent committed 2026-10-15 21:55:45 +00:00
1 parent 97cb771d89
commit e584319eb6
14 files changed
+1293 -79

No files matched your search

+1 -1
View File
@@ -20,7 +20,7 @@ REA_IDA_IMAGE=ghcr.io/alexmi256/rea-agent-ida:6.5.0-linux-amd64
# of the four images you started.
REA_ENGINE=all
REA_VERSION=6.5.0
NODE_VERSION=24.21.0
NODE_VERSION=24.22.0
GHIDRA_VERSION=12.1.4
GHIDRA_ZIP=ghidra_12.1.4_PUBLIC_20260921.zip
# Optional: pin the Ghidra archive digest (see docs/TROUBLESHOOTING.md)
+46 -6
View File
@@ -35,11 +35,11 @@ ARG DEBIAN_FRONTEND=noninteractive
# --- pinned upstream versions ------------------------------------------------
ARG REA_VERSION=6.5.0
ARG NODE_VERSION=24.21.0
ARG NODE_VERSION=24.22.0
# REA declares frida as an optional dependency (JS instrumentation). npm 11
# blocks package install scripts by default, so frida's native binding is
# installed explicitly with --allow-scripts and linked into REA's tree.
ARG FRIDA_VERSION=17.23.2
ARG FRIDA_VERSION=17.23.3
ARG GHIDRA_VERSION=12.1.4
ARG GHIDRA_ZIP=ghidra_12.1.4_PUBLIC_20260921.zip
ARG GHIDRA_SHA256=
@@ -62,8 +62,20 @@ ARG PWNDBG_TAG=2026.09.15
ARG PWNDBG_COMMIT=55596aa676a2756efb8d67034f31675170eabe48
ARG PWTOOLS_VERSION=4.15.0
ARG PYELFTOOLS_VERSION=0.33
# Held at 2.1.2 by pwntools 4.15.0, which declares `unicorn !=2.1.3, !=2.1.4,
# >=2.0.1`: pip's resolver refuses 2.1.4 outright (ResolutionImpossible), so the
# newer unicorn is not installable next to the pwntools build REA admits. Unicorn
# is not REA-admitted either, so pwntools' own constraint is the binding one.
ARG UNICORN_VERSION=2.1.2
ARG ILSPYCMD_VERSION=8.2.0.7535
# Wakaru - REA's caller-supplied JavaScript source-recovery engine. Accepted range
# ^1.13.0, verified against 1.14.0 (src/javascript/recovery/WakaruRelease.ts), and
# REA fingerprints the executed binary, so the digest below is REA's own audited
# build of the official release asset - not a value we invented. The npm package
# would give the same bytes via @wakaru/cli-linux-x64, but REA wants the platform
# binary, never the Node shim in node_modules/.bin.
ARG WAKARU_VERSION=1.14.0
ARG WAKARU_LINUX_X64_SHA256=664fa06782a61eda7ed15885134b52525fd565dd77607e642062ceb6cd927c38
ARG CHROME_DEB_URL=https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
# --- engine selection --------------------------------------------------------
@@ -142,8 +154,9 @@ RUN --mount=type=cache,target=/var/cache/rea-dl,sharing=locked \
cd /var/cache/rea-dl; \
file="node-v${NODE_VERSION}-linux-x64.tar.xz"; \
[ -f "$file" ] || curl -fsSLO "https://nodejs.org/dist/v${NODE_VERSION}/${file}"; \
[ -f SHASUMS256.txt ] || curl -fsSLO "https://nodejs.org/dist/v${NODE_VERSION}/SHASUMS256.txt"; \
grep " ${file}\$" SHASUMS256.txt | sha256sum -c -; \
shasums="SHASUMS256-${NODE_VERSION}.txt"; \
[ -f "$shasums" ] || curl -fsSLo "$shasums" "https://nodejs.org/dist/v${NODE_VERSION}/SHASUMS256.txt"; \
grep " ${file}\$" "$shasums" | sha256sum -c -; \
tar -xJf "$file" -C /usr/local --strip-components=1 --no-same-owner; \
node --version; npm --version
@@ -208,6 +221,33 @@ RUN --mount=type=cache,target=/var/cache/rea-dl,sharing=locked \
install -m 0644 "$jar" "/opt/jadx/${jar}"; \
ls -l "/opt/jadx/${jar}"
# =============================================================================
# 7b. Wakaru 1.14.0 (REA's JavaScript source-recovery engine)
# =============================================================================
# REA advertises `recover_javascript_sources` but installs nothing for it: it
# refuses the capability unless REA_WAKARU_COMMAND is an ABSOLUTE path to a
# platform binary (accepted range ^1.13.0, parser verified with 1.14.0) and
# util-linux prlimit is present. Hence the official release asset rather than the
# npm package - npm yields identical bytes at @wakaru/cli-linux-x64, but its
# node_modules/.bin entry is a Node shim, and REA resolves and fingerprints the
# executable itself. The digest below is REA's own audited value for this asset,
# so the build fails if the bytes ever differ; the banner is asserted in the
# exact form REA parses (`wakaru <semver>`).
ENV REA_WAKARU_COMMAND=/opt/wakaru/wakaru
RUN --mount=type=cache,target=/var/cache/rea-dl,sharing=locked \
set -eux; \
cd /var/cache/rea-dl; \
asset="wakaru-linux-x64.tar.gz"; \
tarball="wakaru-${WAKARU_VERSION}-linux-x64.tar.gz"; \
[ -f "$tarball" ] || curl -fsSLo "$tarball" \
"https://github.com/pionxzh/wakaru/releases/download/v${WAKARU_VERSION}/${asset}"; \
mkdir -p /opt/wakaru; \
tar -xzf "$tarball" -C /opt/wakaru wakaru; \
chmod 0755 /opt/wakaru/wakaru; \
echo "${WAKARU_LINUX_X64_SHA256} /opt/wakaru/wakaru" | sha256sum -c -; \
"$REA_WAKARU_COMMAND" --version; \
test -f /usr/bin/prlimit && test -x /usr/bin/prlimit
# =============================================================================
# 8. Apktool 2.x (REA verified against 2.7.0; 3.x changes output layout)
# =============================================================================
@@ -379,8 +419,8 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
# servers) and supergateway (stdio <-> Streamable HTTP bridge so the
# container's MCP surface can be published on a port).
# =============================================================================
ARG UV_VERSION=0.13.0
ARG SUPERGATEWAY_VERSION=4.1.0
ARG UV_VERSION=0.13.1
ARG SUPERGATEWAY_VERSION=4.2.0
ENV UV_INSTALL_DIR=/usr/local/bin \
UV_NO_MODIFY_PATH=1 \
UV_TOOL_BIN_DIR=/usr/local/bin \
+16 -12
View File
@@ -120,17 +120,18 @@ Full walkthrough: [docs/HERMES-MCP.md](docs/HERMES-MCP.md).
| Family | Tools | Pinned |
| ------ | ----- | ------ |
| REA | `rea-agents` CLI + MCP server | 6.5.0 (Node 24.21.0) |
| REA | `rea-agents` CLI + MCP server | 6.5.0 (Node 24.22.0) |
| Native engines | Ghidra + JDK 21, Hopper (demo or licensed), IDA (BYO) | Ghidra 12.1.4, Hopper 6.4.2 |
| ELF/core analysis | pwntools + pyelftools + Unicorn in a venv, `gdb` + `gdb-multiarch`, pwndbg | pwntools 4.15.0, pyelftools 0.33, Unicorn 2.1.2, pwndbg 2026.09.15 |
| ELF/core analysis | pwntools + pyelftools + Unicorn in a venv, `gdb` + `gdb-multiarch`, pwndbg | pwntools 4.15.0, pyelftools 0.33, Unicorn 2.1.2 (held: pwntools excludes 2.1.3/2.1.4), pwndbg 2026.09.15 |
| Firmware | binwalk (built from source), unblob + extractors (`debugfs`, 7z, squashfs, cpio, dtc, lz4/lzop/lzip, cabextract, ...) | binwalk 3.1.0, unblob 26.6.4 |
| Packets / signals | tshark + capinfos, tcpdump, sigrok-cli + libsigrokdecode | TShark 4.2.2, sigrok-cli 0.7.2 |
| Web / HTTP | mitmdump, HAR + SAZ readers, Google Chrome, Playwright Chromium | mitmproxy 12.2.3, playwright-core 1.58.2 |
| Android | jadx-headless-mcp JAR, Apktool, adb | jadx-headless-mcp 0.7.1, Apktool 2.12.1 |
| Managed code | .NET SDK 8 + ilspycmd | ilspycmd 8.2.0.7535 |
| WASM / EVM | WABT, bundled EVMole (npm) | WABT 1.0.42 |
| Dynamic / cross-arch | Frida (npm binding installed and loadable), qemu-user-static (all targets), wine, cross toolchains (aarch64, arm, mips, mipsel, riscv64, powerpc, mingw-w64) | Frida 17.23.2 binding |
| MCP plumbing | uv (for PyPI MCP servers), supergateway (stdio ↔ Streamable HTTP) | uv 0.13.0, supergateway 4.1.0 |
| JavaScript recovery | Wakaru — unpack bundles and recover readable modules (`recover_javascript_sources`) | Wakaru 1.14.0 (the build REA audited) |
| Dynamic / cross-arch | Frida (npm binding installed and loadable), qemu-user-static (all targets), wine, cross toolchains (aarch64, arm, mips, mipsel, riscv64, powerpc, mingw-w64) | Frida 17.23.3 binding |
| MCP plumbing | uv (for PyPI MCP servers), supergateway (stdio ↔ Streamable HTTP) | uv 0.13.1, supergateway 4.2.0 |
Layer-by-layer detail, and what is deliberately **not** included:
[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
@@ -238,12 +239,15 @@ docs/ images, architecture, BYO licences, IDA editions,
make check-updates # ~5 s: every pin, what upstream ships, what REA admits
```
One row per component, exit code 1 when something needs action. The distinction
it encodes is the whole game: **REA validates engines and helper tools by digest
or version banner, so upstream "latest" is frequently the wrong target** — it
refuses a Hopper build whose launcher digest it has not verified and a Ghidra
release it was not built against. The checker reads REA's admitted versions live
from the pinned release, so bumping REA re-derives them.
One row per component. It exits non-zero only when the image is **wrong now**
(a pin behind what REA admits, a capability REA advertises with no tool
installed, or a new REA release — the trigger to move the pins). A merely-newer
upstream release is listed but waits: the rule here is that pins follow
`rea-agents`, and REA validates engines by digest and helper tools by version
banner, so upstream "latest" is frequently the wrong target — it refuses a
Hopper build whose launcher digest it has not verified and a Ghidra release it
was not built against. The checker reads REA's admitted versions live from the
pinned release, so bumping REA re-derives them.
A weekly CI job runs the same check and fails when something moves; the report
lands in the run summary. Per-component procedures, the release-string files that
@@ -277,9 +281,9 @@ Phases:
| Phase | What it proves |
| ----- | -------------- |
| `tools` | the image's declared engine profile, the engine paths it advertises, and each helper reporting a version REA admits (e.g. WABT must say 1.0.42, sigrok-cli 0.7.2, mitmdump 12.2.3) |
| `tools` | the image's declared engine profile, the engine paths it advertises, and each helper reporting a version REA admits, plus Wakaru's absolute path / executable state / banner / audited digest (e.g. WABT must say 1.0.42, sigrok-cli 0.7.2, mitmdump 12.2.3, wakaru 1.14.0) |
| `analysis` | for the images that have them: `rea doctor --provider ghidra/hopper` healthy; Ghidra analyses **x86-64, aarch64, armv7, MIPS32 BE, MIPS32 LE, RISC-V64, PowerPC, Windows PE32+**; decompiles; string search; Hopper demo-mode analysis of the x86-64 fixture |
| `static` | `inspect-binary-layout` (pwntools), `inspect-wasm-artifact` (WABT), `inspect-sqlite-database`, `inspect-managed-artifact`/`-members` (.NET), `inspect-plist`, `inspect-evm-interface`, `analyze-javascript-application`, `import-reference-source` |
| `static` | `inspect-binary-layout` (pwntools), `inspect-wasm-artifact` (WABT), `recover-javascript-sources` (**Wakaru**, on a bundle fixture), `inspect-sqlite-database`, `inspect-managed-artifact`/`-members` (.NET), `inspect-plist`, `inspect-evm-interface`, `analyze-javascript-application`, `import-reference-source` |
| `capture` | tshark pcap fields/summary, sigrok VCD decode, binwalk regions, unblob extraction, HAR + a **real mitmdump capture**, Chrome CDP target discovery/page/screenshot capture, Playwright scenario (launch+click+DOM), PTY process capture |
| `clients` | Frida device inventory, adb client/devices, Apktool launcher, JADX engine JAR, IDA registration acceptance, JEB endpoint, Hopper state dir |
| `mcp` | a real MCP client (`docker/verify/mcp-probe.mjs`): `initialize` → `tools/list` (~207 tools) → `tools/call` on the first engine-free tool the *installed release* advertises (currently `inspect_binary_layout`) → where the image has an engine, `open_binary` + `analyze_function` + `close_binary` through it; then the Streamable HTTP bridge is started and initialised over HTTP |
+11
View File
@@ -189,6 +189,17 @@ module.exports = { exportReport };
EOF
echo " + jsapp/"
# --- Minified bundle for JavaScript source recovery (Wakaru) ------------------
# A deliberately minified, single-IIFE bundle: this is the shape REA's
# recover_javascript_sources exists for (unpack a production bundle back into
# readable modules), and it is written here rather than downloaded so the
# harness stays offline and deterministic.
mkdir -p "$FIX/jsbundle"
cat >"$FIX/jsbundle/bundle.min.js" <<'EOF'
(function(){"use strict";var n=0;function c(){n++;return "click "+n}function d(a){return a.map(function(x){return x*2})}window.addEventListener("load",function(){document.body.addEventListener("click",c),console.log("app ready",d([1,2,3]))})})();
EOF
echo " + jsbundle/bundle.min.js"
# --- Web page for CDP / Playwright captures ----------------------------------
mkdir -p "$FIX/www"
cat >"$FIX/www/index.html" <<'EOF'
+33
View File
@@ -84,6 +84,39 @@ else
_skip "js application graph" "fixture missing"
fi
section "JavaScript source recovery (Wakaru)"
# REA's own documented chain: recover readable modules from a minified bundle,
# then analyse the recovered tree. The assertions are on schema fields (provider,
# status, per-module status, the fingerprinted executable digest) rather than on
# incidental text, and the digest proves REA ran the audited asset this image pins.
if [ -s "$FIX/jsbundle/bundle.min.js" ]; then
rm -rf "$FIX/recovered"
if rea recover-javascript-sources "$FIX/jsbundle/bundle.min.js" "$FIX/recovered" \
--json >"$FIX/recovery.json" 2>"$FIX/recovery.err"; then
check_json_file "js recovery: provider is Wakaru" '.provider.id == "wakaru"' "$FIX/recovery.json"
check_json_file "js recovery: reported version" '.provider.version | startswith("1.14")' "$FIX/recovery.json"
check_json_file "js recovery: completed with no failures" \
'.normalized_result.status == "complete" and .normalized_result.failed == 0' "$FIX/recovery.json"
check_json_file "js recovery: bundle decompiled into a module" \
'.normalized_result.modules[0].reported_status == "decompiled"' "$FIX/recovery.json"
check_json_file "js recovery: REA fingerprinted the audited Wakaru build" \
'.normalized_result.engine.executable.sha256 == "664fa06782a61eda7ed15885134b52525fd565dd77607e642062ceb6cd927c38"' \
"$FIX/recovery.json"
check "js recovery: published module file is non-empty" "." \
-- bash -c "test -s \"$FIX/recovered/modules/bundle.js\" && wc -c <\"$FIX/recovered/modules/bundle.js\""
check "js recovery: evidence keeps its limitations" "derived artifacts" \
-- bash -c "jq -r '.normalized_result.limitations[0]' \"$FIX/recovery.json\""
# recovered modules are a usable analysis input, which is the point of the
# operation - analyse them with the same tool the jsapp fixture uses
check_rea "analysis of the recovered module tree" analyze-javascript-application "." \
-- "$FIX/recovered/modules" --json
else
_bad "js source recovery" "recover-javascript-sources exited non-zero; see $FIX/recovery.err"
fi
else
_skip "js source recovery" "fixture missing"
fi
section "historical source import"
if [ -d "$FIX/jsapp/src" ]; then
check_rea "reference source import" import-reference-source "." -- "$FIX/jsapp/src" --json
+17
View File
@@ -74,6 +74,23 @@ check "apktool launcher" "2\.[0-9]+" -- apktool --version
check "dotnet sdk" "8\." -- dotnet --version
check "ilspycmd" "ILSpy|[0-9]+\.[0-9]+" -- "$REA_ILSPY_CMD_PATH" --version
section "javascript source recovery (Wakaru)"
# REA's admission rules, asserted one by one: an absolute REA_WAKARU_COMMAND, a
# readable+executable regular file, the banner in the exact form REA parses
# (`wakaru <semver>` inside its accepted range), the audited digest, and
# util-linux prlimit as a regular executable - REA checks all of these before it
# will run the capability at all.
check "REA_WAKARU_COMMAND is absolute" "^/opt/wakaru/wakaru$" \
-- bash -c 'printf "%s\n" "${REA_WAKARU_COMMAND:-unset}"'
check "wakaru binary is a real file, readable and executable" "^/opt/wakaru/wakaru$" \
-- bash -c '[ -f "$REA_WAKARU_COMMAND" ] && [ -x "$REA_WAKARU_COMMAND" ] && realpath "$REA_WAKARU_COMMAND"'
check "wakaru banner in the form REA parses" "^wakaru 1\.14\.0$" \
-- "$REA_WAKARU_COMMAND" --version
check "wakaru digest is REA's audited build" "664fa06782a61eda7ed15885134b52525fd565dd77607e642062ceb6cd927c38" \
-- sha256sum "$REA_WAKARU_COMMAND"
check "util-linux prlimit (REA's resource limiter)" "^/usr/bin/prlimit" \
-- bash -c 'test -f /usr/bin/prlimit && [ -x /usr/bin/prlimit ] && realpath /usr/bin/prlimit'
section "firmware / packet / signal"
check "binwalk" "Binwalk|binwalk|3\." -- binwalk --version
check "unblob" "26\.6" -- "$REA_UNBLOB_COMMAND" --version
+6 -4
View File
@@ -25,17 +25,17 @@ reuses every shared layer from cache. Notable orderings and pins:
| Layer | Contents | Notable pinned versions |
| ----- | -------- | ----------------------- |
| 1 | build/runtime basics: compilers, `gdb`/`gdb-multiarch`, `strace`, `ltrace`, `binutils`, `elfutils`, `yara`, `upx-ucl`, `nasm`, `qemu-user-static`, `binfmt-support`, X11/EGL/GBM/font stack, `xvfb`, `socat`, `tcpdump`, `util-linux` (prlimit) | Ubuntu 24.04 archives |
| 2 | **Node.js** + npm | 24.21.0 (REA needs 22.19+/24.11+/26+) |
| 2 | **Node.js** + npm | 24.22.0 (REA needs 22.19+/24.11+/26+) |
| 3 | **REA** (`rea-agents`) globally installed → `rea` on PATH | 6.5.0 |
| 4 | **WABT** (wasm toolchain) and **jadx-headless-mcp** (Android Java engine) | WABT 1.0.42, jadx-headless-mcp 0.7.1 |
| 4 | **WABT** (wasm toolchain), **jadx-headless-mcp** (Android Java engine) and **Wakaru** (JavaScript source recovery) | WABT 1.0.42, jadx-headless-mcp 0.7.1, Wakaru 1.14.0 |
| 5 | **Apktool** launcher (Java wrapper + jar) | 2.12.1 (2.x line REA verified) |
| 6 | **.NET SDK 8** + `ilspycmd` in `/opt/dotnet-tools` | ilspycmd 8.2.0.7535 |
| 7 | Python venvs: **pwntools**, **unblob**, **mitmproxy** | pwntools 4.15.0 + pyelftools 0.33 + Unicorn 2.1.2; unblob 26.6.4; mitmproxy 12.2.3 |
| 7 | Python venvs: **pwntools**, **unblob**, **mitmproxy** | pwntools 4.15.0 + pyelftools 0.33 + Unicorn 2.1.2 (pwntools excludes 2.1.3/2.1.4); unblob 26.6.4; mitmproxy 12.2.3 |
| 8 | firmware/packet/signal tooling: `binwalk` (built from source in a Rust stage), `tshark`/`capinfos`, `sigrok-cli` + `libsigrokdecode`, `adb`, `e2fsprogs`, `sleuthkit`, `squashfs-tools`, `mtools`, `dtc` | binwalk 3.1.0, TShark 4.2.2, sigrok-cli 0.7.2 |
| 9 | **chromium-family browser**: Google Chrome stable + Playwright Chromium (in `/opt/ms-playwright`) | chrome stable, playwright-core 1.58.2's pinned build |
| 10 | **pwndbg** + its venv (for `--debugger-context` on core files) | pwndbg 2026.09.15, git `55596aa` |
| 11 | optional **wine** + cross toolchains (aarch64, arm, mips/mipsel, riscv64, powerpc, mingw-w64) | Ubuntu 24.04 |
| 12 | **uv** and **supergateway** (stdio↔Streamable HTTP MCP bridge) | uv 0.13.0, supergateway 4.1.0 |
| 12 | **uv** and **supergateway** (stdio↔Streamable HTTP MCP bridge) | uv 0.13.1, supergateway 4.2.0 |
| 13 | non-root user `rea` (uid/gid 1000), writable dirs, `ilspycmd`, entrypoint, healthcheck, verification harness, IDA templates | - |
| **17a** | **Ghidra** + native decompiler for `linux_x86_64` (JDK 21 is layer 1 and lives in every image) — `REA_ENGINE=all\|ghidra` | Ghidra 12.1.4 |
| **17b** | **Hopper** (official public demo `.deb`) + Qt6/Xvfb runtime — `REA_ENGINE=all\|hopper` | Hopper 6.4.2 Linux |
@@ -55,6 +55,7 @@ Always set (engine-independent `ENV`):
JAVA_HOME
REA_PWNTOOLS_PYTHON, REA_PWNDBG_GDB, REA_PWNDBG_GDBINIT, REA_PWNDBG_VENV_PATH
REA_WABT_BIN_DIRECTORY, REA_JADX_MCP_JAR, REA_APKTOOL_COMMAND, REA_ADB_PATH
REA_WAKARU_COMMAND (JavaScript source recovery; needs util-linux prlimit)
REA_BINWALK_COMMAND, REA_UNBLOB_COMMAND, REA_TSHARK_COMMAND, REA_CAPINFOS_COMMAND
REA_SIGROK_COMMAND, REA_MITMDUMP_COMMAND, REA_ILSPY_CMD_PATH
REA_BROWSER_NO_SANDBOX, PLAYWRIGHT_BROWSERS_PATH, REA_MCP_INPUT_SCHEMA_PROFILE
@@ -88,6 +89,7 @@ harness all read `/usr/local/share/rea/engine` to decide which checks apply.
/opt/venvs/{pwntools,unblob,mitmproxy}
/opt/pwndbg pwndbg checkout + .venv
/opt/wabt/bin wasm2wat, wasm-objdump, wasm-validate
/opt/wakaru/wakaru Wakaru 1.14.0, REA's JS recovery engine (REA_WAKARU_COMMAND)
/opt/jadx/<jar> jadx-headless-mcp 0.7.1 (REA_JADX_MCP_JAR)
/opt/dotnet-tools/ilspycmd
/opt/ms-playwright Playwright Chromium build
+4 -4
View File
@@ -188,10 +188,10 @@ minority of it. Measured on this project's build host:
| Image | Size (`docker image inspect .Size`) |
| --- | --- |
| `rea-agent` (Ghidra + Hopper) | 8.39 GiB — 9,008,689,771 bytes |
| `rea-agent-ghidra` | 8.30 GiB — 8,916,518,011 bytes |
| `rea-agent-hopper` | 7.55 GiB — 8,102,797,549 bytes |
| `rea-agent-ida` | 7.46 GiB — 8,010,625,009 bytes |
| `rea-agent` (Ghidra + Hopper) | 8.54 GiB — 9,172,884,021 bytes |
| `rea-agent-ghidra` | 8.46 GiB — 9,080,709,595 bytes |
| `rea-agent-hopper` | 7.70 GiB — 8,266,992,157 bytes |
| `rea-agent-ida` | 7.61 GiB — 8,174,817,477 bytes |
Measured with `make sizes` on this build host (amd64); the byte counts move by a
few tens of KB between rebuilds even with identical inputs (layer timestamps), so
+1 -1
View File
@@ -32,7 +32,7 @@ in `docker/rea.env.example`.
| Run foreign-architecture Linux binaries | `qemu-user-static` for every qemu target (also usable as `qemu-mips -L /usr/mips-linux-gnu ...`) |
| Run Windows PE binaries | `wine` / `wine64` |
| Debug foreign binaries | `gdb-multiarch`, `gdbserver` (via `gdb-multiarch --args`), `qemu-*` user-mode gdbstub (`-g 1234`) |
| Emulate instructions statically | Unicorn 2.1.2 inside REA's pwntools venv (used by `inspect-binary-layout`) |
| Emulate instructions statically | Unicorn 2.1.2 inside REA's pwntools venv (used by `inspect-binary-layout`; held at 2.1.2 because pwntools 4.15.0 excludes 2.1.3/2.1.4) |
| Cross-platform core dumps | `inspect-recorded-crash` interprets x86-64 **and** AArch64 ELF64 cores |
| Non-Linux formats | Mach-O/FAT/FAT64, PE/PE32+, DOS MZ/COM, ELF, raw binary imports, Amiga/C64 containers (Ghidra) |
+1
View File
@@ -15,6 +15,7 @@ keeps its own licence and terms. That installed set is:
| Qt6 runtime (Hopper's dependency) | LGPL-3.0 |
| Hopper (Linux demo package) | commercial; the **public demo** build is redistributed here exactly as downloaded from the vendor |
| WABT | Apache-2.0 |
| Wakaru | Apache-2.0 |
| Apktool | Apache-2.0 |
| jadx-headless-mcp | Apache-2.0 |
| .NET SDK 8 | MIT |
+68 -7
View File
@@ -8,8 +8,14 @@ make check-updates # or: scripts/check-updates.py
```
It reads every pin out of `Dockerfile`, asks each upstream what it currently
ships, and prints one row per component in about five seconds. Exit code is 1
when something needs action, so it works as a gate as well as a report.
ships, and prints one row per component in about five seconds.
Exit status follows the policy below — `1` only when the image is **wrong now**
(a pin older than what REA admits, a capability REA advertises with no tool
installed, repo files that disagree, or a new `rea-agents` release, which is the
trigger to move everything). A merely-newer upstream release is reported as
`OUTDATED` and does **not** fail the check, because by policy it waits for the
next REA release.
| mode | use |
| --- | --- |
@@ -63,6 +69,28 @@ REA re-derives the expectations instead of relying on this document's memory.
## The general loop
### When to bump at all
**Do not bump anything unless REA itself is out of date.** The pins exist to
match the REA release the image ships, and REA validates engines by digest and
helper tools by version banner; a third-party bump made on its own schedule is
how you end up running builds REA never verified. So the trigger is a new
`rea-agents`: read what it admits, move the pins to that, and rebuild.
Two exceptions, and only these:
1. **A drift `BEHIND-REA` on any row** — our pin is older than what the *current*
REA admits, i.e. the image is already wrong. Fix immediately.
2. **A capability gap (`MISSING`)** — REA advertises an operation whose tool we do
not ship. Also already wrong, and it looks like a broken install to a user.
Everything else (`OUTDATED`, `info`) waits for the next REA release. The reason to
hold is not conservatism for its own sake: `docs/VERIFICATION.md` records which
builds were actually exercised together, and bumping out of step invalidates that
without adding any verified capability.
### The loop
1. `make check-updates` — see what moved and why it matters.
2. Bump the pin in the `Dockerfile` (`ARG …`), and in `.env.example` if it is
mirrored there.
@@ -73,8 +101,9 @@ REA re-derives the expectations instead of relying on this document's memory.
make build-ghidra # or build / build-hopper / build-ida
scripts/verify.sh --engine ghidra # full harness for that image
```
5. Update docs that *name* versions (README, `docs/IMAGES.md`,
`docs/VERIFICATION.md`) and the size table if it changed (`make sizes`).
5. Update docs that *name* versions (README, `docs/ARCHITECTURE.md`,
`docs/MULTIARCH.md`, `docs/IMAGES.md`, `docs/VERIFICATION.md`) and the size
table if it changed (`make sizes`).
6. Commit with the upstream URL and the check you ran; push. CI re-validates and
republishes under the new tag.
@@ -198,6 +227,7 @@ the version REA admits.
| unblob | `UNBLOB_VERSION` | `pip install "unblob==…"` into `/opt/venvs/unblob` | `REA_UNBLOB_COMMAND` | `static` |
| WABT | `WABT_VERSION` | GitHub release tarball + sha256, `/opt/wabt` | `REA_WABT_BIN_DIRECTORY` | `static` |
| jadx-headless-mcp | `JADX_MCP_VERSION` | GitHub release jar, `/opt/jadx` | `REA_JADX_MCP_JAR` | `static` |
| Wakaru | `WAKARU_VERSION` + `WAKARU_LINUX_X64_SHA256` | GitHub release asset, `/opt/wakaru` | `REA_WAKARU_COMMAND` | `tools` + `static` |
| apktool | `APKTOOL_VERSION` | GitHub release jar + wrapper script | `REA_APKTOOL_COMMAND` | `static` |
| pwntools / pyelftools / unicorn | `PWTOOLS_VERSION` / `PYELFTOOLS_VERSION` / `UNICORN_VERSION` | one venv, `/opt/venvs/pwntools` | `REA_PWNTOOLS_PYTHON` | `static` |
| TShark / capinfos | — (apt) | `apt-get install tshark` | `REA_TSHARK_COMMAND`, `REA_CAPINFOS_COMMAND` | `capture` |
@@ -212,6 +242,20 @@ Notes:
(`wabt-<ver>-linux-x64.tar.gz`, verified against its published `.sha256`).
* **Apktool is deliberately held at 2.x.** REA's floor is 2.7.0 and 3.x changes
the output layout; the checker reports 3.x as optional rather than required.
* **Wakaru is pinned twice on purpose**: `WAKARU_VERSION` (the release) and
`WAKARU_LINUX_X64_SHA256` (the exact asset). REA fingerprints the executed
binary and its docs record the digest of the build it audited, so a matching
version number is not enough — the checker compares our digest with REA's own
documented value (`wakaru binary digest` row), and the harness re-checks it in
the image. Install the **release asset**, never the `node_modules/.bin` shim.
* **Some pins are held by another pin, and that is not an oversight.** Unicorn is
the live example: 2.1.4 is newer upstream, but pwntools 4.15.0 declares
`unicorn !=2.1.3, !=2.1.4`, so pip refuses it (`ResolutionImpossible`) — the
build fails loudly rather than installing something inconsistent. The checker
reports these as `info` with `held on <pin>: <reason>` (the `HELD` map in
`scripts/check-updates.py`) instead of as an available update. When you add an
entry there, put the constraint in the `Dockerfile` comment too: the build and
the checker should tell the same story.
* **ilspycmd has no admitted version** — REA only requires a runnable binary, so
the checker reports a newer NuGet release as information. Every `.NET` fixture
in the harness exercises it, so bumping is cheap but not free.
@@ -248,11 +292,15 @@ with the reason, so the row is expected rather than surprising.
| gap | why |
| --- | --- |
| Wakaru (`recover_javascript_sources`) | not installed yet — REA admits ^1.13.0; the procedure above is the fix |
| IDA / JEB | licensable vendor software: bring-your-own by design, never downloaded |
| Hopper licensed builds | same; only the vendor demo package is installed |
| Apple composite providers (plist etc.) | provider-scoped, not a host capability |
Wakaru was the first real `MISSING` row produced by the checker — REA 6.5.0
advertised `recover_javascript_sources` while the image shipped nothing for it, so
the capability reported `provider_missing`. The procedure above is what fixed it;
the section stays as the worked example.
---
## E. Our own tools (no REA gate)
@@ -307,8 +355,10 @@ and it is separate from the image build on purpose: a red `updates` run means
* the markdown report lands in the run summary
(Actions → *updates* → the run → **Summary**);
* the JSON report is uploaded as the `update-check` artifact;
* the job **fails when anything is actionable** — the failure is the reminder,
and it names the component plus both candidates;
* the job **fails when the image is wrong now** — a pin behind what REA admits, a
capability REA advertises with no tool installed, repo files that disagree, or a
new REA release. Merely-newer upstream releases are listed in the summary as
waiting for the next release and do not fail the run;
* it warns when a row could **not be checked** (`unknown`). An unverifiable row
is not a pass: it means a lookup failed, so that run proved nothing about the
component. The job sets `GH_TOKEN` so GitHub-sourced rows use the
@@ -342,6 +392,17 @@ passes when it cannot see upstream is worse than no check.
## Traps worth remembering
* **The download cache is shared across builds and keyed by filename, not by
version.** `/var/cache/rea-dl` is a `--mount=type=cache` target: `[ -f x ] ||
curl … x` skips the download when `x` is already there. So any cached file whose
name does not carry the version is *stale the moment the pin moves* — Node's
`SHASUMS256.txt` failed this way during the 24.21→24.22 bump ("no properly
formatted checksum lines found", because the cached file still described the old
tarball). Fix the name (`SHASUMS256-${NODE_VERSION}.txt`), don't delete the
cache: a version-qualified name makes the workspace self-healing and keeps the
warm cache useful. Check for this pattern on every cached download — the ones
that are deliberately version-less (Google Chrome's `google-chrome.deb`, "always
current stable") are the exception, not the rule.
* **`config_drift` from `rea doctor` is not an install failure.** It usually
means the engine build is newer than the admitted one.
* **A Hopper version number proves nothing** — the gate is the launcher digest.
+2
View File
@@ -141,6 +141,8 @@ only `REA_VERSION`.
| **CI, GitHub-hosted runner (quick profile)** | **59 passed / 0 failed / 13 skipped** — run [38474640500](https://github.com/alexmi256/rea-agent-docker/actions/runs/38474640500), ghidra image built on the runner in 8m52s, harness executed as the runner's uid (not the image's), artifact `verification-report-ghidra` uploaded. This run is also the regression test for the two defects CI found: the entrypoint dying on a foreign uid, and the un-gated Hopper state-dir check. |
| **CI publish run (all four, REA 6.4.0)** | run [38481777484](https://github.com/alexmi256/rea-agent-docker/actions/runs/38481777484) — every engine image built on a hosted runner (ida 10m0s, hopper 9m10s, ghidra 11m42s, all-engines longest), quick harness green on each, then published to `ghcr.io/alexmi256/rea-agent[-ghidra\|-hopper\|-ida]` with tags `6.4.0-linux-amd64` and `latest`. Public access verified independently of the CI log: `docker manifest inspect` on all four tags succeeds **anonymously** (no `ghcr.io` credentials on the machine doing the check). |
| **Local per-engine sweep, REA 6.5.0 (this bump)** | all 107/0/3 · ghidra 95/0/8 · hopper 88/0/7 · ida 80/0/11 — **0 failures on all four**, exit 0 on every image. The first pass caught one harness defect (`sigrok decoder inventory` asserted `uart` appeared in the decoder catalog's default 50-of-111 page); fixed by searching with `inspect-signal-decoders --query uart` and re-run. Also newly green relative to 6.4.0: sqlite, wasm, go-binary, pcap ×3, signal ×2, frida, and the RISC-V64/PowerPC ELF searches. |
| **Local per-engine sweep, dependency round (2026-10-15)** | all **120/0/3** · ghidra **108/0/8** · hopper **101/0/7** · ida **93/0/11** — 0 failures on all four, exit 0 on every image; full log in [`docs/verify-engines.txt`](verify-engines.txt). This is the round that bumped Node to 24.22.0, frida to 17.23.3, uv to 0.13.1, supergateway to 4.2.0 and added **Wakaru 1.14.0**, so the count includes 13 new checks: five assert REA's admission rules for the Wakaru binary (absolute path, readable+executable, the `wakaru <semver>` banner, the audited SHA-256, util-linux prlimit) and eight run the operation itself — a minified bundle fixture is recovered, the Evidence is asserted on schema fields (provider `wakaru`, `status: complete`, `failed: 0`, module `decompiled`, and the fingerprinted digest **`664fa067…` proving REA ran the audited asset**), the published module file is checked, and the recovered tree is then analysed with `analyze-javascript-application`. |
| **Build failures this round caught** | Two, both real and both fixed rather than worked around: the Node layer died with `sha256sum: no properly formatted checksum lines found` because `/var/cache/rea-dl` held the *previous* version's `SHASUMS256.txt` under a version-less name (the cached filename is now version-qualified, and the stale file was deliberately left in place to prove the fix, not the cache clean); and `pip` refused `unicorn==2.1.4` with `ResolutionImpossible` because pwntools 4.15.0 declares `unicorn !=2.1.3, !=2.1.4` — so unicorn stays at 2.1.2 and the checker now reports it as `held on 2.1.2: <reason>` instead of as an available update. Neither reached GHCR: the publish step runs only after verification. |
The committed summary of the last full single-image run is in
[`docs/verify-summary.txt`](verify-summary.txt); the per-engine table from the
+980 -30
View File
File diff suppressed because it is too large. Load diff
+107 -14
View File
@@ -14,6 +14,15 @@
# 1. the newest thing upstream ships, and
# 2. what the pinned REA release actually ADMITS.
#
# Exit status follows the project's update policy (docs/UPDATES.md):
# 1 = the image is wrong NOW - a pin is older than what REA admits
# (`BEHIND-REA`), REA advertises a capability with no tool installed
# (`MISSING`), repo files disagree (`MISMATCH`), or a new REA release exists
# (`NEW REA`, the trigger to move everything).
# 0 = nothing wrong. `OUTDATED` rows (newer upstream, nothing REA admits) are
# reported and deliberately do NOT fail: by policy they wait for the next
# rea-agents release.
#
# (2) is the one that matters for the engine and helper tools. REA 6.5.0 admits
# Ghidra 12.1.4, Hopper 6.4.2-by-digest, Binwalk 3.1.x, Unblob 26.6.x, WABT
# 1.0.42, sigrok-cli 0.7.2, TShark 4.x, jadx-headless-mcp 0.7.1, pwntools
@@ -63,11 +72,24 @@ REA_SOURCES = {
"tools": "src/application/CapabilityInventory.ts",
}
# statuses that mean "do something"
ACTIONABLE = {"outdated", "behind-rea", "missing", "mismatch"}
# Statuses that mean "the image is wrong right now" - these fail the check.
# `outdated` is deliberately NOT here: this project's policy is that a newer
# upstream release waits for the next REA release, so a merely-newer tool is
# reported but must not turn the weekly job red (see docs/UPDATES.md).
ACTIONABLE = {"behind-rea", "missing", "mismatch", "new-rea"}
# Reported, never failing: newer upstream, no REA release shipped it yet.
PENDING = {"outdated"}
# Components deliberately held back from a newer upstream, with the reason. These
# are reported as `info` ("held") rather than as pending updates, so a reader can
# see the decision instead of assuming an oversight.
HELD = {
"unicorn": "pwntools 4.15.0 declares unicorn !=2.1.3,!=2.1.4 - pip refuses 2.1.4",
}
STATUS_LABEL = {
"ok": "ok",
"outdated": "OUTDATED",
"new-rea": "NEW REA",
"behind-rea": "BEHIND-REA",
"ahead": "ahead",
"missing": "MISSING",
@@ -380,8 +402,10 @@ def check_rea(fetcher: Fetcher, pins: dict[str, str], admissions: ReaAdmissions)
row.status = "unknown"
row.note = "could not compare versions"
elif order < 0:
row.status = "outdated"
row.hint = "bump REA_VERSION, then rebuild and run the harness (see docs/UPDATES.md)"
row.status = "new-rea"
row.hint = ("a new REA is the trigger for everything else: read what it admits "
"(the columns below re-derive), move the pins to match, rebuild and "
"run the harness (docs/UPDATES.md)")
node_engines = str((data.get("engines") or {}).get("node", ""))
if node_engines:
row.note = f"engines.node: {node_engines}"
@@ -597,6 +621,12 @@ def tool_row(
order = compare(pinned, row.upstream)
if order is not None and order < 0 and row.status == "ok":
if key in HELD:
# newer upstream, but something we ship forbids it - say so, and never
# dress it up as an available update
row.status = "info"
row.note = ((row.note + "; ") if row.note else "") + f"held on {pinned}: {HELD[key]}"
return row
if floor_semantics and admitted:
# REA states a minimum, not a target: anything at or above the floor
# is admitted, so a newer upstream is optional rather than required.
@@ -684,19 +714,59 @@ def check_tools(fetcher: Fetcher, pins: dict[str, str], admissions: ReaAdmission
row.admits = None
rows.append(row)
# Wakaru: REA advertises javascript recovery, we do not install it
admitted = admissions.tool("wakaru")
# Wakaru: the JavaScript recovery engine REA advertises. Installed since
# 6.5.0-era tooling; the upstream is the GitHub release asset the image
# fetches (npm mirrors the identical version).
wakaru_pin = pins.get("WAKARU_VERSION", "")
url = "https://api.github.com/repos/pionxzh/wakaru/releases/latest"
upstream, error = None, None
try:
upstream = str(npm_latest(fetcher, "wakaru")["version"])
upstream = github_latest(fetcher, "pionxzh/wakaru")
except Exception as exc: # noqa: BLE001
error = exc
rows.append(tool_row("wakaru", "wakaru (npm)", "", admitted,
"https://registry.npmjs.org/wakaru/latest", upstream, error,
pinned_present=False))
rows.append(tool_row("wakaru", "wakaru (GitHub release)", wakaru_pin,
admissions.tool("wakaru"), url, upstream, error,
pinned_present=bool(wakaru_pin)))
return rows
def check_wakaru_digest(fetcher: Fetcher, pins: dict[str, str], admissions: ReaAdmissions) -> list[Row]:
"""Our Wakaru pin must be the asset REA audited, byte for byte.
REA fingerprints the executed binary and docs/javascript-recovery.md records
the SHA-256 of the build it verified, so a different asset would be running
bytes REA never audited even though the version number matches.
"""
pinned = pins.get("WAKARU_LINUX_X64_SHA256", "")
url = REA_RAW.format(repo=REA_REPO, version=admissions.version,
path="docs/javascript-recovery.md")
row = Row("wakaru-digest", "wakaru binary digest", "REA-admitted tools", "ok",
pinned=(pinned[:12] + "…") if pinned else None, source=url)
if not pinned:
row.status = "missing"
row.note = "no WAKARU_LINUX_X64_SHA256 pin, so the fetched asset is unverified"
row.hint = "pin the digest REA documents for the verified release"
return [row]
try:
doc = fetcher.text(url)
except Exception as exc: # noqa: BLE001
return [unknowable(row, exc)]
match = re.search(r"SHA-256\s*`([0-9a-f]{64})`", doc)
if not match:
return [unknowable(row, RuntimeError("no SHA-256 in REA's javascript-recovery doc"))]
documented = match.group(1)
row.admits = documented[:12] + "…"
release = re.search(r"wakaru/releases/download/v([\d.]+)/", doc)
if documented == pinned:
row.note = ("matches the digest REA documents for Wakaru "
f"{release.group(1) if release else ''}".strip())
else:
row.status = "behind-rea"
row.note = f"REA documents {documented} for Wakaru {release.group(1) if release else '?'}"
row.hint = "install the release asset REA audited, or REA runs unaudited bytes"
return [row]
def check_misc(fetcher: Fetcher, pins: dict[str, str], admissions: ReaAdmissions) -> list[Row]:
rows: list[Row] = []
@@ -826,6 +896,7 @@ def main() -> int:
lambda: check_hopper(fetcher, pins, admissions),
lambda: check_java(fetcher, pins, admissions),
lambda: check_tools(fetcher, pins, admissions),
lambda: check_wakaru_digest(fetcher, pins, admissions),
lambda: check_misc(fetcher, pins, admissions),
lambda: check_actions(fetcher),
]
@@ -842,6 +913,7 @@ def main() -> int:
rows = [r for r in rows if not any(n in r.key.lower() or n in r.group.lower() for n in needles)]
actionable = [r for r in rows if r.status in ACTIONABLE]
pending = [r for r in rows if r.status in PENDING]
unknowns = [r for r in rows if r.status == "unknown"]
stamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
@@ -862,6 +934,7 @@ def main() -> int:
"summary": {
"rows": len(rows),
"actionable": len(actionable),
"pending": len(pending),
"unknown": len(unknowns),
},
"rows": [r.as_dict() for r in rows],
@@ -875,16 +948,29 @@ def main() -> int:
print()
print(f"Pins read from `Dockerfile`; REA admission tables read from "
f"`morluto/rea@rea-agents-{rea_version}`. "
f"**{len(actionable)} actionable**, {len(unknowns)} unverifiable, {len(rows)} rows.")
f"**{len(actionable)} actionable**, {len(pending)} awaiting the next REA release, "
f"{len(unknowns)} unverifiable, {len(rows)} rows.")
print()
if actionable:
print("**Actionable — the image disagrees with what REA admits:**")
print()
print("| status | component | pinned | upstream | REA admits |")
print("| --- | --- | --- | --- | --- |")
for row in actionable:
print(f"| **{STATUS_LABEL[row.status]}** | {row.label} | {row.pinned or '-'} | "
f"{row.upstream or '-'} | {row.admits or '-'} |")
print()
else:
print("Nothing actionable: every pin matches what upstream and REA currently expect.")
print("Nothing actionable: every pin matches what REA currently expects.")
print()
if pending:
print("Newer upstream, reported but not applied — by policy these wait for the "
"next `rea-agents` release:")
print()
print("| component | pinned | upstream |")
print("| --- | --- | --- |")
for row in pending:
print(f"| {row.label} | {row.pinned or '-'} | {row.upstream or '-'} |")
return 1 if actionable and not args.no_fail else 0
print(f"rea-agent-docker update check — {stamp}")
@@ -926,12 +1012,19 @@ def main() -> int:
print(f" {'':<{width}} {row.note}")
if row.status in ACTIONABLE and row.hint:
print(f" {'':<{width}} -> {row.hint}")
elif row.status in PENDING and row.note:
print(f" {'':<{width}} = newer upstream only - per policy this waits "
"for the next REA release")
print()
print(f"{len(rows)} rows: {len(actionable)} actionable, "
f"{len(unknowns)} unverifiable, {len(rows) - len(actionable) - len(unknowns)} fine")
print(f"{len(rows)} rows: {len(actionable)} actionable, {len(pending)} awaiting the next "
f"REA release, {len(unknowns)} unverifiable, "
f"{len(rows) - len(actionable) - len(pending) - len(unknowns)} fine")
if actionable:
print("See docs/UPDATES.md for what to check and how to apply each bump.")
elif pending:
print("Nothing wrong: the pins match what REA admits. The newer upstream releases above "
"wait for the next rea-agents release (docs/UPDATES.md).")
return 1 if actionable and not args.no_fail else 0