dsh-plugin-rea 1.0.5 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/check-updates.ps1 CHANGED
@@ -46,6 +46,10 @@ function Test-NodeEngines([string]$node, [string]$engines) {
46
46
  # 之后 $node.Major 为 $null,所有 ^ / ~ 分支都会判假(曾因此把 Node 24 判成不满足)。
47
47
  $v = Get-NumericVersion $node
48
48
  if (-not $v) { return $null }
49
+ # 只要有一个 term 的形态不认识,整个判断就只能给"无法解析":返回 $false 会把
50
+ # `22.x` / `>=22` 这类合法但未实现的 pattern 误报成"不满足",让用户去升级一个
51
+ # 本来就满足的 Node。
52
+ $recognized = $true
49
53
  foreach ($term in ($engines -split '\|\|')) {
50
54
  $t = $term.Trim()
51
55
  if ($t -match '^\^(\d+\.\d+\.\d+)$') {
@@ -56,8 +60,11 @@ function Test-NodeEngines([string]$node, [string]$engines) {
56
60
  } elseif ($t -match '^~(\d+\.\d+\.\d+)$') {
57
61
  $base = [version]$Matches[1]
58
62
  if ($v.Major -eq $base.Major -and $v.Minor -eq $base.Minor -and $v -ge $base) { return $true }
63
+ } else {
64
+ $recognized = $false
59
65
  }
60
66
  }
67
+ if (-not $recognized) { return $null }
61
68
  return $false
62
69
  }
63
70
 
@@ -107,8 +114,10 @@ if ($installed -and $latest) {
107
114
  } elseif ($vi -and $vl -and $vl -eq $vi -and $installed -ne $latest) {
108
115
  Warn "同版本号但通道不同:$installed → $latest(预发布/正式)"
109
116
  } else { Ok '已是最新版本' }
110
- } else {
117
+ } elseif (-not $latest) {
111
118
  Warn '未能取到上游最新版本(两个 registry 都查不通?)—— 已跳过版本比对'
119
+ } else {
120
+ Warn '本地没读到已安装版本(见上面那条)—— 已跳过版本比对'
112
121
  }
113
122
 
114
123
  # ------------------------------------------------------------- Node 运行时
@@ -214,6 +223,67 @@ if (Test-Path $profPatch) {
214
223
  }
215
224
  }
216
225
 
226
+ # ------------------------------------------------- 一体包(bundle 分支)
227
+ # 设计文档承诺了这个分区:一体包把组件版本钉在 manifest 里,供应结果记在
228
+ # bundle.json 里。两者不一致 = 有一次 pin 更新还没重新供给,或盘上的组件被换过。
229
+ Head '一体包供给状态(bundle 分支)'
230
+ $pluginRoot = $PSScriptRoot
231
+ $manifestPath = Join-Path $pluginRoot 'manifest\bundle.manifest.json'
232
+ $bundleRoot = $env:REA_BUNDLE_ROOT
233
+ if (-not $bundleRoot) {
234
+ $homeDir = $env:DSH_HOME
235
+ if (-not $homeDir) { $homeDir = Join-Path $HOME '.dsh' }
236
+ $bundleRoot = Join-Path $homeDir 'plugins\rea-bundle'
237
+ }
238
+ $bundleJson = Join-Path $bundleRoot 'bundle.json'
239
+
240
+ if (-not (Test-Path $manifestPath)) {
241
+ Info '未找到 manifest/bundle.manifest.json —— 这不是一体包分支,跳过'
242
+ } else {
243
+ Info "bundle 根目录: $bundleRoot"
244
+ try {
245
+ $manifest = Get-Content $manifestPath -Raw -Encoding UTF8 | ConvertFrom-Json
246
+ if (Test-Path $bundleJson) {
247
+ $state = Get-Content $bundleJson -Raw -Encoding UTF8 | ConvertFrom-Json
248
+ Ok "已供给:$($state.provisionedAt)"
249
+ $drift = 0
250
+ foreach ($name in $manifest.pins.PSObject.Properties.Name) {
251
+ if ($name -eq 'nodeEngines') { continue }
252
+ $pinned = [string]$manifest.pins.$name
253
+ $installed = ''
254
+ if ($state.versions -and ($state.versions.PSObject.Properties.Name -contains $name)) {
255
+ $installed = [string]$state.versions.$name
256
+ }
257
+ if (-not $installed) { continue }
258
+ # Node 的 pin 是引擎范围,不是版本号;其它组件比前缀即可
259
+ $same = $installed.Equals($pinned) -or $installed.StartsWith(($pinned -split '\.')[0] + '.')
260
+ if ($same) { Info " $name = $installed(pin $pinned)" }
261
+ else { Warn "$name 盘上为 $installed,manifest 已钉 $pinned —— 重跑 setup 对齐"; $drift++ }
262
+ }
263
+ if ($drift -eq 0) { Ok '所有组件与 manifest 的 pin 一致' }
264
+ if ($state.unverifiedChecksums -and @($state.unverifiedChecksums).Count -gt 0) {
265
+ Warn ("TOFU(无厂商校验和,取首次实测值): " + (@($state.unverifiedChecksums) -join ', '))
266
+ }
267
+ $mirrored = Join-Path $bundleRoot 'bin\rea-shim.mjs'
268
+ if (Test-Path $mirrored) { Ok 'MCP 行指向的 shim 已就位' }
269
+ else { Bad "缺少 MCP 行指向的 shim: $mirrored —— 重跑 setup --reuse" }
270
+ # 镜像副本的 setup 用它自己所在的位置推 plugin root,所以 manifest 必须
271
+ # 一起镜像。旧版镜像缺这个文件:auto-provision 会以 ENOENT 崩掉且不可见,
272
+ # 而且它已写下的 lock 会让重试被抑制 6 小时。
273
+ $mirroredManifest = Join-Path $bundleRoot 'manifest\bundle.manifest.json'
274
+ if (Test-Path $mirroredManifest) { Ok '镜像副本的 manifest 已就位' }
275
+ else { Bad "镜像副本缺少 manifest: $mirroredManifest —— 请从插件目录重跑一次 node scripts/setup.mjs(旧镜像无法自愈)" }
276
+ } else {
277
+ Warn "尚未供给:$bundleJson 不存在 —— 运行 node scripts/setup.mjs(或直接装插件,行会自行触发)"
278
+ }
279
+ $pins = @()
280
+ foreach ($name in $manifest.pins.PSObject.Properties.Name) {
281
+ if ($name -ne 'nodeEngines') { $pins += "$name=$($manifest.pins.$name)" }
282
+ }
283
+ Info ('manifest 钉子: ' + ($pins -join ', '))
284
+ } catch { Warn "bundle 状态解析失败: $($_.Exception.Message)" }
285
+ }
286
+
217
287
  # ------------------------------------------------------------------ 汇总
218
288
  Head '结论'
219
289
  if ($actions.Count -eq 0) { Ok '无需动作:整条链路与上游保持一致' }
package/cordis.patch.yml CHANGED
@@ -1,33 +1,31 @@
1
- # dsh-plugin-rea — REA (Reverse Engineer Anything) as native DSH tools
1
+ # dsh-plugin-rea — one-plugin bundle
2
2
  #
3
- # Configuration-only bundle: no Host/Client entry files. It inserts one row of
4
- # the official MCP client plugin and points it at the installed `rea mcp`
5
- # stdio server; the model then sees mcp__rea__<tool>.
3
+ # Registers the REA MCP server as mcp__rea__* and launches it through the
4
+ # bundle's own shim, which reads the machine-local state written by
5
+ # `scripts/setup.mjs` and exports exactly the variables REA reads.
6
6
  #
7
- # ---------------------------------------------------------------------------
8
- # PORTABLE BY DEFAULT — this file carries no machine-specific path.
7
+ # PORTABLE BY CONSTRUCTION — no machine path appears in this file.
8
+ # * `command` is the running Node executable (`process.execPath`), so the
9
+ # launcher needs no PATH entry and no shim resolution by the OS.
10
+ # * `args[0]` is computed at boot from DSH_HOME, where setup mirrors the shim.
11
+ # * FORWARD SLASHES ONLY in the JS string literals below. A single-quoted YAML
12
+ # scalar keeps backslashes literal, and JavaScript then eats them: '\plugins'
13
+ # is not "\plugins" but the BEL control character, '\rea-bundle' becomes a
14
+ # carriage return and '\bin' a backspace, so the path is silently destroyed.
15
+ # Forward slashes are accepted on Windows and are correct on POSIX, so one
16
+ # expression serves every platform.
17
+ # * `path`/`require` are not in scope for a `!!js` expression (`ctx` is the
18
+ # loader scope), hence plain string concatenation.
19
+ # * No DSH_HOME (undefined/absent/empty) falls back to the SAME rule the shim
20
+ # and setup.mjs use — `<home>/.dsh` — never to the bare home directory.
21
+ # Getting this wrong is silent: setup would mirror the shim into
22
+ # `<home>/.dsh/plugins/rea-bundle/bin/` while the row looked for it one level
23
+ # up, so the row could never start on a machine that merely happened to have
24
+ # DSH_HOME unset.
9
25
  #
10
- # Prerequisite: `rea` must be reachable from the PATH the harness sees.
11
- # (`npm i -g rea-agents` puts it there.)
12
- #
13
- # Windows : command: cmd.exe args: ['/c', 'rea', 'mcp'] (as below)
14
- # macOS/Linux: command: 'rea' args: ['mcp']
15
- # → see examples/cordis.patch.posix.yml
16
- #
17
- # Analysis engines: REA reads GHIDRA_INSTALL_DIR from its environment.
18
- # - Option 1 (recommended): set it as an OS-level environment variable,
19
- # then start/restart the harness so the MCP child inherits it.
20
- # - Option 2: put an `env:` block in the row —
21
- # see examples/cordis.patch.with-env.yml.
22
- # Without it, native analysis reports the engine as unavailable while every
23
- # other REA capability keeps working. Static JavaScript/Electron and .NET
24
- # analysis need no engine at all.
25
- #
26
- # Only ONE row may use serverName: rea in a profile. To adapt this bundle for
27
- # your machine without touching the upstream file, override `mcp-rea` from
28
- # your profile's own cordis.patch.yml (id-targeted config override) — that
29
- # file is never published.
30
- # ---------------------------------------------------------------------------
26
+ # One row may use serverName: rea in a profile. To pin a different launch
27
+ # command on this machine, override `mcp-rea` from the profile's own
28
+ # cordis.patch.yml (id-targeted config override) — that file is never published.
31
29
 
32
30
  - insert:
33
31
  - id: mcp-rea
@@ -35,9 +33,19 @@
35
33
  config:
36
34
  serverName: rea
37
35
  transport: stdio
38
- command: 'cmd.exe'
36
+ command: !!js process.execPath
39
37
  args:
40
- - '/c'
41
- - 'rea'
42
- - 'mcp'
38
+ # DOUBLE-QUOTED on purpose. A bare scalar containing "? " and ": " is not
39
+ # one scalar at all: YAML reads `? a : b` as an explicit key/value pair,
40
+ # so js-yaml hands the loader a mapping instead of an expression node and
41
+ # args[0] becomes an object the launcher cannot use. Quoting makes the
42
+ # scalar unambiguous; the JS string literals inside use single quotes so
43
+ # they cannot collide with the YAML ones.
44
+ - !!js "(process.env.DSH_HOME && process.env.DSH_HOME.trim() !== '' ? process.env.DSH_HOME : (process.env.USERPROFILE || process.env.HOME || '') + '/.dsh') + '/plugins/rea-bundle/bin/rea-shim.mjs'"
45
+ - mcp
46
+ env:
47
+ # Only meaningful when the harness itself runs on Electron; harmless
48
+ # for a plain Node host. Explicit is better than relying on this
49
+ # variable happening to survive the child-env scrub.
50
+ ELECTRON_RUN_AS_NODE: '1'
43
51
  failOnStartupError: false
@@ -0,0 +1,186 @@
1
+ # one-plugin bundle — design notes
2
+
3
+ Goal: a fresh machine installs **one DSH plugin** and gets a working `mcp__rea__*`
4
+ tool surface, without first installing Node, REA, a JDK, or Ghidra by hand.
5
+
6
+ `main` keeps the original **config-only adapter** (a single MCP row pointing at a
7
+ `rea` the user installed themselves). This branch adds the self-provisioning
8
+ variant next to it. Nothing on `main` changes.
9
+
10
+ ---
11
+
12
+ ## 1. What upstream will and will not do
13
+
14
+ REA is explicit: it never installs or upgrades unrelated software, and every
15
+ dependency is "caller-supplied" (`SKILL.md`, `install.sh`, and the remediation
16
+ strings in `dist/`). That is a deliberate upstream boundary, not an oversight.
17
+ Consequently **all provisioning must live on our side**, and it must not pretend
18
+ to be REA.
19
+
20
+ Two upstream rules shape the runtime wiring:
21
+
22
+ - Only `GHIDRA_INSTALL_DIR` + `JAVA_HOME` (and `REA_IDA_MCP_CONFIG`,
23
+ `HOPPER_LAUNCHER_PATH`) are ever published by `rea setup`. Every other knob —
24
+ `REA_ADB_PATH`, `REA_APKTOOL_COMMAND`, `REA_JADX_MCP_JAR`,
25
+ `REA_BROWSER_EXECUTABLE`, `REA_ILSPY_CMD_PATH` — must be supplied by the
26
+ launching environment. We supply them per machine from one JSON file.
27
+ - REA hard-requires an **absolute** Node/runtime path in practice: it rejects
28
+ relative paths for `GHIDRA_INSTALL_DIR` and resolves Windows `java.exe` by
29
+ walking PATH itself. We therefore never rely on PATH at all.
30
+
31
+ ## 2. Verified facts this design rests on
32
+
33
+ Measured on Windows 11 x64, REA 6.4.0, Ghidra 12.1.4, Temurin JDK 25.0.4:
34
+
35
+ | Fact | Evidence |
36
+ | --- | --- |
37
+ | A row can launch REA with **no PATH `rea`** | `node <pkg>/scripts/rea.mjs mcp` answered the MCP `initialize` handshake (`serverInfo.name = rea`, version 6.4.0) and `rea doctor --json` reported 170 tools, `healthy: true` |
38
+ | `!!js` expressions are supported in this profile layer | the existing profile patch already used one; the loader evaluates them |
39
+ | A row-level `env:` block reaches the MCP child | `binary_session` reported `registrationEnvironment` = `GHIDRA_INSTALL_DIR`, `JAVA_HOME` |
40
+ | No fixed home/layout is promised | the disk layout differs per install; nothing may assume one |
41
+ | Windows is an explicit POC boundary upstream | `DoctorDiagnostics`: "the experimental Windows x64 Ghidra P0 boundary" |
42
+
43
+ ## 3. Architecture
44
+
45
+ Three responsibilities, kept strictly apart:
46
+
47
+ 1. **Shim (runtime, per launch).** A tiny Node script the MCP row points at. It
48
+ loads the machine-local `bundle.json`, exports the discovered paths into the
49
+ child environment, and execs REA. It never downloads anything on the normal
50
+ path and never writes to the plugin directory.
51
+ 2. **Setup (provisioning, explicit or first-run).** A Node script that resolves
52
+ the component manifest, downloads what is missing into a cache, verifies
53
+ checksums, unpacks, runs the readiness probe, and writes `bundle.json`.
54
+ Idempotent and re-runnable; `--reuse` adopts pre-existing installs.
55
+ 3. **Patch (wiring, published).** `cordis.patch.yml` from this branch, carrying
56
+ **no machine path**: the row points at `<plugin>/bin/rea-shim.mjs` and the
57
+ shim is responsible for the rest.
58
+
59
+ The published plugin therefore stays portable, while the machine-specific truth
60
+ lives in one small JSON file under the DSH home.
61
+
62
+ ```
63
+ <dsh home>/plugins/rea-bundle/
64
+ bundle.json # machine-local truth: resolved paths + versions + hashes
65
+ mirror.json # which plugin checkout the mirrored files came from
66
+ cache/ # downloaded archives (resumable, checksum-verified)
67
+ vendor/ # unpacked payloads (node, jdk, ghidra, apktool, jadx, adb)
68
+ logs/setup.log
69
+ ```
70
+
71
+ ## 4. Why a shim instead of an `!!js` expression
72
+
73
+ `!!js` can inject values into the row at load time, but it cannot:
74
+ download, verify, unpack, run a readiness probe, or report progress. It also
75
+ couples the published patch to expressions that must parse on every platform.
76
+ A shim is boring, testable, and can report an action's clear remediation text
77
+ through `failOnStartupError: false` without breaking the harness.
78
+
79
+ ## 5. Control flow
80
+
81
+ ```
82
+ dsh starts
83
+ └─ MCP row: node <bundle root>/bin/rea-shim.mjs (setup mirrors it there)
84
+ ├─ bundle.json present & components verify → exec REA (normal path)
85
+ ├─ missing, auto-provision allowed → start setup detached, print
86
+ │ remediation, exit non-zero
87
+ └─ missing, auto-provision refused → print exact command, exit
88
+ ```
89
+
90
+ The shim resolves its bundle root from **its own location**, not from `DSH_HOME`:
91
+ the harness strips every `DSH_*` name from an MCP child's environment, so with a
92
+ custom `DSH_HOME` the shim would look in `~/.dsh`, find nothing, and start a second
93
+ provisioning run there.
94
+
95
+ `setup` stages, each idempotent and individually verifiable:
96
+
97
+ 1. `detect` — host platform/arch, DSH home, plugin dir, existing installs
98
+ 2. `resolve` — manifest entry per component; candidate URLs are ordered
99
+ 3. `fetch` — download to cache with resume + retries; skip if checksum matches
100
+ 4. `verify` — sha256 against the manifest (and vendor checksum file when present)
101
+ 5. `unpack` — platform-native extraction, atomic into `vendor/`
102
+ 6. `wire` — write `bundle.json`, export the environment variables REA reads
103
+ 7. `mirror` — copy the launcher to the path the published patch computes
104
+ 8. `probe` — `rea doctor --json`, then launch the **mirrored** shim exactly as the row would
105
+ 9. `report` — human summary + exit code; every failure names its remediation
106
+
107
+ Two notes on stages that exist because their absence was a bug:
108
+
109
+ - **7 (mirror).** The patch points at `<bundle root>/bin/rea-shim.mjs`, and the
110
+ plugin itself is not guaranteed to live anywhere the patch can compute. Setup
111
+ therefore copies the launcher (and the setup script it re-invokes) into the
112
+ bundle root. Without this, provisioning could succeed and the row would still
113
+ fail — and the "install the plugin first, it provisions itself" path could
114
+ never start, because the shim that does the provisioning would not exist at
115
+ the path being launched. The copy list is exactly what a mirrored run reads
116
+ back, **including `manifest/bundle.manifest.json`**: a mirrored `setup.mjs`
117
+ resolves its plugin root to the bundle root, so `loadManifest()` reads
118
+ `<bundle>/manifest/bundle.manifest.json` — a file only this stage can create.
119
+ It also writes `<bundle>/mirror.json`, naming the checkout the files came
120
+ from: the bundle outlives any particular checkout location, and without that
121
+ record a later mirrored run can only copy the bundle onto itself (provisioning
122
+ the manifest it was mirrored with, forever) and its remediation text would
123
+ point the operator at a copy that cannot start.
124
+ - **The row's `!!js` scalar is quoted.** A bare scalar containing `? ` and `: `
125
+ is not one scalar: YAML reads it as an explicit key/value pair, so the loader
126
+ receives a mapping where an expression belongs — measured against the
127
+ harness's own dialect, `args[0]` became `{"[object Object]": "..."}` and the
128
+ row could not launch at all. Any future expression with a ternary needs the
129
+ quotes; `audit-manifest.mjs` rejects the bare form.
130
+ - **3 (fetch).** Route order is deliberate and environment-dependent: direct
131
+ first, then the system proxy, and on Windows the .NET route ahead of both
132
+ because it follows the GitHub release-asset redirect that the Node routes
133
+ cannot. Neither "always direct" nor "always proxy" is correct — measured on
134
+ the development machine, the local proxy answers github.com but returns 403
135
+ for nodejs.org and 404 for dl.google.com, while the reverse also happens.
136
+
137
+ ## 6. What is genuinely impossible on Windows
138
+
139
+ Declared up front so no release note over-promises. These are **upstream
140
+ host gates**, not configuration gaps:
141
+
142
+ | Capability | Gate |
143
+ | --- | --- |
144
+ | Hopper provider | macOS only |
145
+ | IDA provider | requires a user-owned IDA + `REA_IDA_MCP_CONFIG`; REA never launches it |
146
+ | firmware `binwalk` / `unblob` | Linux only |
147
+ | Wakaru JS recovery | Linux x64 only |
148
+ | pwntools paths (ELF layout, recorded crash) | Linux x64 only |
149
+ | pwndbg / gdb core context | Linux/macOS |
150
+ | `mitmdump` native capture decode | Linux only (HAR needs no engine) |
151
+ | Apple toolchain (Mach-O, plist, asset catalog, LLDB) | macOS only |
152
+ | process capture (`capture_process_scenario`) | Linux/macOS PTY |
153
+
154
+ Everything else — native Ghidra analysis, static JS/Electron, .NET, archives,
155
+ Android (JADX/apktool/adb), browser scenarios, EVM, HAR — is reachable on
156
+ Windows and is what this bundle targets to complete.
157
+
158
+ ## 7. Component matrix (Windows x64 first)
159
+
160
+ | Component | Needed for | Provisioned as |
161
+ | --- | --- | --- |
162
+ | Node runtime | running REA at all | reuse a compatible existing node, else portable `node.exe` |
163
+ | `rea-agents` | the MCP server itself | vendored copy under `vendor/rea-agents` |
164
+ | Temurin JDK 21/25 | Ghidra headless + JADX bridge | portable JDK zip (full JDK, not a JRE) |
165
+ | Ghidra 12.1.x | native analysis | official release zip, extracted |
166
+ | apktool | Android resource decode | `apktool.bat` + jar in one directory |
167
+ | jadx-headless-mcp jar | Android class/method decompile | pinned 0.7.1 jar (REA validates its classes) |
168
+ | Android platform-tools | `adb` device tools | official zip |
169
+ | a Chromium-family browser | browser/CDP scenarios | reuse an installed Edge/Chrome; never downloaded |
170
+ | `ilspycmd` (optional) | .NET decompilation | optional; only wired when present |
171
+
172
+ Ghidra itself is **not** redistributed inside the plugin package: the release is
173
+ ~570 MB, which no npm package can carry. It is fetched from the official release
174
+ and checksum-verified, or adopted from an existing install with `--reuse`.
175
+
176
+ ## 8. Honest limits of this branch
177
+
178
+ - npm's per-package limits (unpacked/compressed size) rule out shipping the JDK
179
+ and Ghidra inside the tarball. "One plugin install" therefore means *one
180
+ install plus an automatic, checksum-verified fetch*, not a 600 MB npm package.
181
+ - First run is network- and time-bound: the full payload is roughly 800 MB
182
+ (Ghidra 570 MB, JDK 205 MB) plus the REA package. Progress is reported and the
183
+ download is resumable; a pre-seeded cache makes it offline-capable.
184
+ - Version pins age. `manifest/bundle.manifest.json` carries the pins, and
185
+ `check-updates.ps1` gains a bundle section so drift is reported rather than
186
+ discovered later.