summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/docs
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-02 10:58:21 -0400
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-02 10:58:21 -0400
commit002e3d7d13c414ea874abbee8d82803dd3a72fe5 (patch)
tree6627c12fa07f6a1cf73e5aaeb7390e698dd3a896 /debug/risc-v-gsp-rm-vm/docs
parent2f249817df327f5dcccf0464ad517fa60d6071d3 (diff)
downloadopen-iov-002e3d7d13c414ea874abbee8d82803dd3a72fe5.tar.gz
open-iov-002e3d7d13c414ea874abbee8d82803dd3a72fe5.zip
debug: green-eye wave 3 - RM harness, local simulator, device map, MCP plugin, value-validation demos, CI pipeline, toolchain docs; add host RM tools rmtool/rmtop
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/docs')
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md25
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/03-SCRIPTING.md3
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/04-PLUGINS.md6
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/05-GHIDRA-BRIDGE.md4
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/08-ROADMAP.md9
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/14-ADVERSARIAL-INTEROP-GUIDE.md7
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/16-CUSTOM-SIMULATOR.md156
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/17-RM-HARNESS.md164
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/18-HARNESS-AUTOMATION.md284
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/19-CONTROL-DISCOVERY.md445
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/20-MCP-SERVER.md277
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/21-CI-RELEASE-PIPELINE.md320
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/22-PORTING-CUDA-AND-DEVICES.md353
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/23-RM610-READINESS.md259
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/24-DEVICE-MAP.md207
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/25-VALUE-VALIDATION.md337
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/26-ARCHITECTURE.md449
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/27-TOOL-AUDIT.md252
18 files changed, 3537 insertions, 20 deletions
diff --git a/debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md b/debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md
index f1e1946..3b56512 100644
--- a/debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md
+++ b/debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md
@@ -42,11 +42,17 @@ whole stack scriptable, and QEMU is just the first execution backend.
| Session recording + `analyze` report | working, live-verified |
| Declarative probe specs | working, live-verified |
| Plugin loader + example plugin | working |
-| Test suite | 22 tests green (live QEMU smoke included) |
+| Native RV64IMAC simulator (M1, `--backend native`) | working; QEMU-parity differential test |
+| Native exec/access trace (M1.5, R4): `--trace`, analyzer coverage | working; trace blob + determinism test |
+| Test suite | 155 tests green (live QEMU + native differential included) |
| Ghidra bridge | scripts + docs |
| Real GSP `.fwimage` probe (first-fault report) | working (honest: it does not boot without a TU104 model — see 06) |
-| TCG full-memory tracing plugin | source + optional build |
-| MCP server / custom simulator / full device model | roadmap only (08) |
+| TCG full-memory tracing plugin | source + optional build (verified live) |
+| Automation harness (mutation/portability/version diff) + value demos | working (18/19/21/25) |
+| Host RM oracle harness (`rmharness`) | wrappers + capture working; device tables depend on driver permissions (17) |
+| First-touch device map + lenient native MMIO | working (24) |
+| MCP server (`plugins/mcp_server.py`) | working (20); stdlib-only, read-only by default |
+| Native M2+ device model | roadmap only (08, 16) |
## Quick start
@@ -84,7 +90,18 @@ Interactively:
| [13-GHIDRA-CONTROL](13-GHIDRA-CONTROL.md) | driving green-eye from Ghidra |
| [14-ADVERSARIAL-INTEROP-GUIDE](14-ADVERSARIAL-INTEROP-GUIDE.md) | adversarial firmware navigation |
| [15-AGENTIC-GSP-IDE](15-AGENTIC-GSP-IDE.md) | agentic workflows and runbooks |
-| [16-CUSTOM-SIMULATOR](16-CUSTOM-SIMULATOR.md) | non-QEMU simulator design (later) |
+| [16-CUSTOM-SIMULATOR](16-CUSTOM-SIMULATOR.md) | native simulator (M1/M1.5 implemented; M2+ design) |
+| [17-RM-HARNESS](17-RM-HARNESS.md) | host RM oracle harness (`rmharness`) |
+| [18-HARNESS-AUTOMATION](18-HARNESS-AUTOMATION.md) | mutation/portability automation |
+| [19-CONTROL-DISCOVERY](19-CONTROL-DISCOVERY.md) | unknown-call discovery and reachability |
+| [20-MCP-SERVER](20-MCP-SERVER.md) | bundled MCP server plugin |
+| [21-CI-RELEASE-PIPELINE](21-CI-RELEASE-PIPELINE.md) | release triage pipeline |
+| [22-PORTING-CUDA-AND-DEVICES](22-PORTING-CUDA-AND-DEVICES.md) | porting playbook |
+| [23-RM610-READINESS](23-RM610-READINESS.md) | RM 610 readiness checklist |
+| [24-DEVICE-MAP](24-DEVICE-MAP.md) | first-touch MMIO device map |
+| [25-VALUE-VALIDATION](25-VALUE-VALIDATION.md) | value-validation demos s01–s10 |
+| [26-ARCHITECTURE](26-ARCHITECTURE.md) | full architecture and API surface map |
+| [27-TOOL-AUDIT](27-TOOL-AUDIT.md) | tool audit: purpose, tests, usage, status |
| [GREEN-EYE-ROADMAP](../GREEN-EYE-ROADMAP.md) | long-term program H1–H7 |
## Where this fits in the Open-IOV workflow
diff --git a/debug/risc-v-gsp-rm-vm/docs/03-SCRIPTING.md b/debug/risc-v-gsp-rm-vm/docs/03-SCRIPTING.md
index c677c8d..89c61c4 100644
--- a/debug/risc-v-gsp-rm-vm/docs/03-SCRIPTING.md
+++ b/debug/risc-v-gsp-rm-vm/docs/03-SCRIPTING.md
@@ -4,7 +4,8 @@
green-eye scripts are ordinary Python programs run with
`./greeneye.sh run <script.py>` (or `python3 -m greeneye run …`). The
-names `ge`, `session`/`s`, `memory`, `symbols`, `recorder` are preloaded.
+names `ge`, `session`/`s`, `memory`, `symbols`, `recorder` are preloaded, and
+`__file__` points at the script path (use it to locate bundled assets).
## 1. Attach or launch
diff --git a/debug/risc-v-gsp-rm-vm/docs/04-PLUGINS.md b/debug/risc-v-gsp-rm-vm/docs/04-PLUGINS.md
index fa0fe0c..db9ee27 100644
--- a/debug/risc-v-gsp-rm-vm/docs/04-PLUGINS.md
+++ b/debug/risc-v-gsp-rm-vm/docs/04-PLUGINS.md
@@ -82,9 +82,9 @@ changes behind plugin-level flags and document them in the session metadata
(`s.recorder.meta`).
**(d) AI/harness integration** — export sessions/reports to external tools,
-generate probe specs, score runs. **MCP servers are planned as a plugin**
-(`docs/08-ROADMAP.md`), not part of v0.1; do not add MCP dependencies to the
-core package before that rung is scheduled.
+generate probe specs, score runs. The **MCP server is implemented as a bundled
+plugin** (`../plugins/mcp_server.py`, see [20-MCP-SERVER](20-MCP-SERVER.md)):
+stdlib-only, read-only by default, no dependency added to the core package.
## 4. Example plugin
diff --git a/debug/risc-v-gsp-rm-vm/docs/05-GHIDRA-BRIDGE.md b/debug/risc-v-gsp-rm-vm/docs/05-GHIDRA-BRIDGE.md
index 857102e..f7f46c3 100644
--- a/debug/risc-v-gsp-rm-vm/docs/05-GHIDRA-BRIDGE.md
+++ b/debug/risc-v-gsp-rm-vm/docs/05-GHIDRA-BRIDGE.md
@@ -83,6 +83,10 @@ $GHIDRA/support/analyzeHeadless <proj_dir> <project> -process <program> -noanaly
`ExportForGreenEye.java` is the reverse helper: it walks the current program
and writes a green-eye-compatible `addr<TAB>name<TAB>size` TSV.
+`greeneye.ghidra.render_ghidra_command(proj_dir, project, program, hits_tsv)`
+renders the `analyzeHeadless … MarkGreenEyeHits.java` command line above for
+scripts (the helper is not wired into the CLI).
+
## 5. Breakpoints from Ghidra
Two practical routes today:
diff --git a/debug/risc-v-gsp-rm-vm/docs/08-ROADMAP.md b/debug/risc-v-gsp-rm-vm/docs/08-ROADMAP.md
index 10f4f5f..5d8a9b1 100644
--- a/debug/risc-v-gsp-rm-vm/docs/08-ROADMAP.md
+++ b/debug/risc-v-gsp-rm-vm/docs/08-ROADMAP.md
@@ -68,6 +68,10 @@ only after the guest-bridge work (PLAN B) no longer depends on it.
## R3. MCP server plugin
+**Status: implemented.** The bundled plugin is `plugins/mcp_server.py`; see
+[20-MCP-SERVER](20-MCP-SERVER.md) for the live tool list, transports and
+capability flags. The text below is the original design sketch.
+
**Why:** expose green-eye sessions to AI harnesses and other agents under the
plugin model the user requested.
@@ -78,8 +82,9 @@ session/analysis API as MCP tools: `list_sessions`, `open_session`,
no network exposure by default. The plugin depends on an MCP library only at
plugin load time; the core stays stdlib-only.
-**Gate:** v0.1 must be stable (acceptance list in 00) and the session format
-frozen. Explicitly deferred by the owner; do not add MCP code before then.
+**Gate:** v0.1 stability and the session format. Implemented stdlib-only
+(no MCP library), stdio/loopback only, read-only unless `--allow-write` /
+`--allow-inject` are passed.
## R4. Full-memory trace (TCG plugin integration)
diff --git a/debug/risc-v-gsp-rm-vm/docs/14-ADVERSARIAL-INTEROP-GUIDE.md b/debug/risc-v-gsp-rm-vm/docs/14-ADVERSARIAL-INTEROP-GUIDE.md
index d310f22..fb78265 100644
--- a/debug/risc-v-gsp-rm-vm/docs/14-ADVERSARIAL-INTEROP-GUIDE.md
+++ b/debug/risc-v-gsp-rm-vm/docs/14-ADVERSARIAL-INTEROP-GUIDE.md
@@ -19,8 +19,11 @@ surface, plus items explicitly labelled roadmap; see also docs/06-ACCURACY.md.
|---|---|
| `probe`/`record`/`analyze`/`compare`/`symbols`/`inject`/`ghidra`, hooks, watchpoints, snapshots, sessions, `export_hits` | working (v0.1) |
| Ghidra control bridge (`run_until`, breakpoints, hits) | working (v0.1) |
-| Full TCG access trace (`qemu/greeneye_trace.c`) | source + optional build; not folded into sessions yet (R4) |
-| Mutation harness (AFL++ class), device model, MCP | roadmap (R5, H2/R1, H7/R3) |
+| Full TCG access trace (`qemu/greeneye_trace.c`) | source + optional build (verified live); native equivalent folds into sessions (`--trace`, R4); direct TCG JSONL import is still roadmap |
+| Mutation harness, control discovery/reach, portability/version diff | working (docs/18, 19, 21, 22) |
+| First-touch device map + lenient native MMIO (H2.1) | working (docs/24) |
+| MCP server (`plugins/mcp_server.py`, H7/R3) | working (docs/20) |
+| Full device model (H2.2) | roadmap (docs/16, 24) |
## 2. The adversary model (a moving target, not a malicious party)
diff --git a/debug/risc-v-gsp-rm-vm/docs/16-CUSTOM-SIMULATOR.md b/debug/risc-v-gsp-rm-vm/docs/16-CUSTOM-SIMULATOR.md
index 2b77ca3..2f99f7b 100644
--- a/debug/risc-v-gsp-rm-vm/docs/16-CUSTOM-SIMULATOR.md
+++ b/debug/risc-v-gsp-rm-vm/docs/16-CUSTOM-SIMULATOR.md
@@ -9,10 +9,116 @@ backend: a small deterministic RISC-V machine (`libgreenmachine`) implementing
the same `Session` contract as today's QEMU backend. It expands
`GREEN-EYE-ROADMAP.md` §H3 (H3.1–H3.4) and the §H6 soft-processor annex.
-**Nothing here exists today.** The only supported backend is `QemuTarget` +
-`RspClient` (`greeneye/target.py`, `greeneye/rsp.py`); native is gated behind
-H3 entry — H2.2, the QEMU device model that makes the real GSP image reach its
-message loop — and must not be started ad hoc. This document only scopes it.
+**Beyond M1.5 (§1.1), nothing here exists yet.** The only production backend is
+`QemuTarget` + `RspClient` (`greeneye/target.py`, `greeneye/rsp.py`); M2+ of
+the native backend is gated behind H3 entry — H2.2, the QEMU device model that
+makes the real GSP image reach its message loop — and must not be started ad
+hoc. This document scopes it.
+
+### 1.1 M1 status (implemented in this tree)
+
+M1 was explicitly commissioned and is implemented behind `--backend native`;
+the status marker above still applies to the remaining milestones (M2–M6) and
+to any use on real GSP images.
+
+* **What exists.** A pure-Python, stdlib-only RV64IMAC interpreter under
+ `sim/` (`sim/decode.py` decoder, `sim/machine.py` 256 MiB RAM + NS16550A
+ UART + execution loop) with an `RspClient`-shaped adapter in
+ `greeneye/sim_backend.py` (`SimClient`, `SimTarget`, `launch_native`).
+ `greeneye.session.Session` runs unchanged on it: registers by name, memory,
+ SW/HW breakpoints, watchpoints, step/continue/interrupt, `run_until`/`call`.
+* **Firmware parity.** The bundled `uproc_demo` boots unmodified; a scripted
+ hook/watch/leave scenario produces an event-for-event identical profile on
+ QEMU and native (`tests/test_sim_backend.py::DifferentialTests`), including
+ handler arguments and return values. Watchpoints stop *before* the accessing
+ instruction, which reproduces QEMU's `watch` stop and the Session
+ clear/step/re-arm dance exactly.
+* **Deviations from this design.** M1 here is the *reference* interpreter, not
+ the Rust product core of §12; no MMU, FPU, GC, snapshots/moments, map DSL,
+ scheduler, event bus or injection ABI (those are M2–M5). There is no
+ self-modifying code: the per-PC decode cache is invalidated by debugger
+ writes only.
+* **Time model.** Virtual time is instruction count: `continue_(timeout)`
+ executes at most `timeout * 1_000_000` instructions (SIM_IPS), then reports
+ a budget timeout. `Session.run(duration=...)` therefore bounds native runs
+ by virtual time, deterministically; QEMU's wall-clock execution naturally
+ reaches different heartbeat counts in the same duration (see §14).
+* **Measured performance** (one host thread, CPython 3.12, this machine):
+ ~2.3 MIPS tight RAM loop, ~1.5 MIPS sustained demo workload
+ (`examples/scripts/sim_bench.py`), ~1.25 MIPS with a hook + watchpoint
+ active. The ≥10 MIPS M1 target in §14 is a compiled-core target and is not
+ met by the Python reference; the demo self-test completes in ~2k
+ instructions, so the parity suite still runs in seconds.
+* **Stop semantics.** `ebreak` (the demo's `rpc_fault`) stops *at* the trap as
+ a SIGTRAP like the gdbstub, and `step` executes over it; `wfi`, `fence` and
+ `fence.i` are no-ops (single hart, no ordering model); unmapped accesses and
+ illegal instructions stop as SIGSEGV/SIGILL stops instead of raising.
+
+### 1.1a M1.5 status - native exec/access trace (implemented in this tree)
+
+The roadmap R4 full-trace path now exists natively, without the QEMU TCG
+plugin and without leaving the process:
+
+* **Engine.** `sim/trace.py` (`Trace`, `make_trace`) collects compact JSONL
+ lines from the execution loop in `sim/machine.py`:
+ `{"t":"exec","vcpu":0,"pc":"0x...","vt":N}` and
+ `{"t":"read"|"write","vcpu":0,"addr":"0x...","size":S,"pc":"0x...","vt":N}`,
+ ending in the plugin's `{"t":"summary",...}` line. `vt` is the machine's
+ deterministic instruction count; nothing reads the host clock.
+* **Knobs (same semantics as `qemu/greeneye_trace.c`).** `exec=on|off`
+ (default on in the CLI), `start=`/`end=` inclusive PC filters applied to the
+ executing instruction - so memory accesses are filtered by *instruction* PC
+ exactly as the plugin filters at translation time - and a ring budget
+ `max_events` after which lines are counted as `dropped` (default 1,000,000,
+ `0` = unlimited).
+* **Delivery.** The trace is written at Session close as the session blob
+ `blobs/trace.jsonl` (referenced from `session.json` as `trace_blob`, with
+ `trace` config/counts in the metadata), so it never swamps `events.jsonl`;
+ folded `exec`/`access`/`read`/`write` events in `events.jsonl` are also
+ accepted by the analyzer.
+* **Analyzer.** `greeneye analyze` renders an `## Execution trace` section:
+ exec/read/write totals, unique PCs grouped by symbol, and accessed 4 KiB
+ pages with read/write counts. `greeneye compare` adds a trace-stream totals
+ delta and includes trace writes in the written-page delta.
+* **API.** `ge.launch(..., backend="native", trace=...)` and
+ `launch_native(trace=...)` take `True`, a dict
+ `{"exec", "start", "end", "max_events"}`, or a `sim.trace.Trace`.
+* **Measured cost.** On the tight one-instruction loop microbenchmark the
+ always-present `if trace` guard costs ~7% versus an unguarded copy; on the
+ demo workload (best-of runs) untraced throughput is unchanged within noise
+ (1.53 MIPS before, 1.55 MIPS after). Tracing itself costs ~2.4x on the demo
+ workload and ~3x on the microbenchmark. `examples/scripts/sim_bench.py
+ --trace` reports both.
+
+### 1.2 Usage
+
+```bash
+./greeneye.sh demo --backend native # the canned demo, native backend
+./greeneye.sh run examples/scripts/trace_rpc.py --backend native
+printf 'bp rpc_sum\nc 0.2\nquit\n' | ./greeneye.sh attach --backend native
+./greeneye.sh record --backend native --trace --duration 0.5
+./greeneye.sh record --backend native --trace --trace-exec off \
+ --trace-start 0x80000000 --trace-end 0x80010000
+./greeneye.sh analyze dumps/<stamp>-record # renders the trace coverage section
+python3 examples/scripts/sim_bench.py --trace # throughput + trace overhead
+python3 examples/scripts/sim_diff.py # QEMU/native profile diff
+```
+
+```python
+import greeneye as ge
+s = ge.launch(kernel="firmware/uproc_demo/build/uproc_demo.elf", backend="native",
+ record="dumps/traced", trace={"exec": True, "start": 0x80000000})
+s.hook("rpc_sum", on_enter=lambda hit: print(hit.arg(0), hit.arg(1)))
+s.watch(s.symbols.lookup("gsp_ctrl") + 0x10, size=8, kind="w")
+s.run(duration=1.0)
+s.close() # writes blobs/trace.jsonl
+```
+
+`--backend` is accepted by `demo`, `run`, `attach`, `record`, `inject` and
+`ghidra`; the default stays `qemu` and the QEMU path is behaviourally
+unchanged. `attach --port` stays QEMU-only (there is no server to attach to),
+and `--trace` is native-only (QEMU uses the TCG plugin). Everything below
+remains the design for M2+.
## 2. Why a custom simulator
@@ -147,6 +253,13 @@ class Backend(Protocol):
Compatibility decisions:
+* **M1 implementation note.** Instead of the `Backend` protocol and typed
+ `Session(backend=...)` sketched below, M1 implements the narrower, already
+ exercised `RspClient` method surface (`greeneye/sim_backend.py`) and leaves
+ `Session` untouched. The `Backend` refactor is deferred until a second
+ non-RSP backend (e.g. Verilator, §16) actually needs it; §5's table is
+ still the M2+ target.
+
* `Session(client=...)` keeps working; `Session(backend=...)` is added and
`client=` becomes an alias for `RspBackend(client)`. `Memory` is re-typed
against `Backend`; its public methods do not change.
@@ -291,16 +404,17 @@ single producer for stop-based and full-access events:
|---|---|---|
| `session_start`, `image_loaded`, `hook`, `watch`, `note`, `exit`, `session_end` | existing | unchanged |
| `bp_hit`, `leave`, `watch_hit`, `stop`, `step`, `mem_read`, `mem_write`, `snapshot*`, `probe*`, `plugin*` | existing | unchanged; `mem_*` may now come from the bus, not only tooling |
-| `exec` | new (M1, optional) | `pc`, `insn`, `vt`, `core`, `symbol` |
-| `access` | new (M2) | `pc`, `addr`, `size`, `rw`, `region`, `vt`, `core` |
+| `exec` | native M1.5 (blob) | `pc`, `vt` (+ `symbol` when folded into an event) |
+| `access` | native M1.5 (`read`/`write` lines or folded events) | `pc`, `addr`, `size`, `vt`; `rw` only for the folded `access` form |
| `device` | new (M2) | `device`, `name`, `rw`, `value`, `vt` |
| `irq` / `schedule` | new (M3) | `line`, `state`, `core`, `vt`, `reason` |
| `inject_*` | existing shape (docs/12) | `service_injected`, `call_enter`, `call_return` unchanged |
`t` stays the recorder's monotonic seconds **filled from virtual time**, so
the same run yields the same file. `exec`/`access` volume is controlled by
-profile logging and a ring budget; the analyzer gains coverage/heatmap
-rendering (roadmap R4). `greeneye compare` treats backend identity, `t` and
+profile logging and a ring budget; for the native backend that is implemented
+(M1.5, §1.1a): the stream is a session blob and the analyzer renders coverage
+and accessed pages. `greeneye compare` treats backend identity, `t` and
host paths as non-semantic, so cross-backend comparison is first-class.
## 11. Injection ABI
@@ -352,12 +466,20 @@ familiar with the codebase; milestones are sequential.
| # | Scope | Exit criteria | Effort |
|---|---|---|---|
| M1 | RV64IMAC interpreter, RAM, NS16550A-compatible UART, loader; `Session` adapter with `Backend` | bundled demo uproc boots unmodified; the canned instrumentation script (`03 §12`) produces the same hook/watch/stop timeline and memory snapshots as QEMU (canonical comparison); determinism test green | 4–6 wk |
+| M1.5 | Native exec/access trace (R4 equivalent): `--trace`, filters, blob + analyzer coverage | trace determinism test green; analyzer renders unique PCs/pages; non-trace path unregressed (`sim_bench.py --trace`) | 1–2 wk |
| M2 | Memory-map DSL + `falcon`/`csb`/`ptimer` devices + profile validation; native region snapshots; moments v1 | a re-hosted uproc build boots from a profile with no core changes; moment save/restore round-trips; restoring reproduces the event-stream hash | 4–6 wk |
| M3 | Deterministic scheduler (budgets, timers, IRQ events) + event log/replay | multi-task test profile replays identically from the log on two hosts; tie-break and replay tests in CI | 3–4 wk |
| M4 | Device-model parity with H2.2 + `dma`/`pmu_stub` + boot shims | real GSP image reaches the same stable state as the QEMU H2 exit under the same profile; differential suite passes for probe and boot fixtures; L3 parity documented, L2 still not claimed | 6–10 wk |
| M5 | Injection ABI + service registry + `log`/`stub`/`forward` interception | an SDK v1 blob loads and serves a control end-to-end; each interception mode has a recorded acceptance session; fuzzing seed/restore demonstrated | 4–6 wk |
| M6 | Performance/ergonomics: profiling, fast paths, `--backend native`, docs + flag-removal criteria | §14 targets met; `greeneye demo --backend native` in the differential CI job; user docs updated | 3–4 wk |
+> **M1 status: done (pure-Python reference implementation).** Ported without
+> the `Backend` refactor (see §5 note); differential parity and a determinism
+> test are in `tests/test_sim_backend.py`; performance is §1.1. **M1.5 (native
+> trace) is done** in §1.1a: `sim/trace.py`, `tests/test_sim_trace.py`,
+> analyzer coverage rendering and `examples/scripts/sim_diff.py`. M2–M6 above
+> are unchanged.
+
Total: roughly 24–36 engineering-weeks, consistent with the roadmap's
"months; explicitly later" classification; none of it starts before the H3
entry gate, and if H2 slips this plan waits.
@@ -389,6 +511,24 @@ harness, trended in CI):
| MMIO access vs RAM access | ≤5× | M2 |
| Event bus throughput | ≥1 M events/s buffered, lossless under budget | M3 |
+> **M1 measured (pure-Python reference).** 2.3 MIPS tight RAM loop, 1.5 MIPS
+> sustained demo workload, 1.25 MIPS with a hook + watchpoint active
+> (`examples/scripts/sim_bench.py`, CPython 3.12). The ≥10 MIPS M1 row is a
+> compiled-core gate and remains open for the Rust core; the Python reference
+> exists to pin semantics and differentially test the firmware, which it does
+> at parity. QEMU executes comparatively more instructions per wall second on
+> this host, so equal wall-clock `run` durations produce different heartbeat
+> counts across backends — the hook/watch/leave event profile is identical.
+>
+> **M1.5 trace overhead (same host).** The tracing guard is a single local
+> boolean test per instruction plus one per memory access; A/B against an
+> otherwise identical loop copy: ~2.4 MIPS vs ~2.6 MIPS on a pathological
+> one-instruction loop (~7%), while the demo workload is unchanged within
+> run-to-run noise (1.53 vs 1.55 MIPS best-of). With `exec=on` the demo
+> workload runs at ~0.54 MIPS (~2.4x slower); an access-only trace is
+> proportionally cheaper. Use `--trace-start/--trace-end` to bound the logged
+> region, and the default 1M-event ring budget to bound memory.
+
## 15. CI strategy
* The Python package and QEMU path remain the default, always-on suite; no
diff --git a/debug/risc-v-gsp-rm-vm/docs/17-RM-HARNESS.md b/debug/risc-v-gsp-rm-vm/docs/17-RM-HARNESS.md
new file mode 100644
index 0000000..59db437
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/17-RM-HARNESS.md
@@ -0,0 +1,164 @@
+# 17 — RM host oracle harness (`rmharness`)
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status.
+> The RM tools are read-only host diagnostics. They are development tooling:
+> never shipped, never present in a guest, never a driver dependency.
+
+## 1. Purpose
+
+The GSP emulation can only be trusted against ground truth. Two of the
+metrics in [06-ACCURACY](06-ACCURACY.md) need the real host RM as a reference:
+
+* **M1 — inventory coverage**: which controls/engines the real RM exposes;
+* **M6 — memory side-effects / fixed points**: real VRAM/heap/BAR1 numbers
+ and engine inventory at a moment in time.
+
+`rmharness` packages the host-side oracle tools and wires their output into
+green-eye sessions, so a simulator/emulation session and a host RM capture
+can be recorded, diffed and cited together. It is the host half of the
+accuracy loop (H4.5), not a boot path or a replacement for PLAN B traces.
+
+## 2. Tool inventory
+
+| tool | source | role in the loop |
+|---|---|---|
+| `rmtool` | `tools/rmtool/` (new) | CLI oracle: `gpu list/topology/info`, `engine list/info`, `mem stats`, `nv2080 class list`, allowlisted `nv2080 method call`, `sample timer`; `--format=table\|json\|csv` |
+| `rmtop` | `tools/rmtop/` (new) | ncurses dashboard over the same query layer; `--once` renders table/json/csv for capture |
+| `rmq` | `tools/rmq/` (pre-existing, unmodified) | FIFO/INTERNAL device info tables for TU104 static data |
+
+Both new tools share one ioctl layer (`tools/rmtool/src/rm_api.c`,
+`query.c`): version check, `NV01_ROOT_CLIENT` -> `NV01_DEVICE_0` ->
+`NV20_SUBDEVICE_0` alloc ladder, and `NV_ESC_RM_CONTROL` queries. The
+device-open protocol follows what `nvidia-smi` does: open
+`/dev/nvidia<N>`, `NV_ESC_WAIT_OPEN_COMPLETE`, then allocate on
+`/dev/nvidiactl`. The ABI structs match the running driver (the in-tree
+userspace SDK snapshot is older and its 40-byte `NVOS64` is rejected).
+
+## 3. Build
+
+```bash
+make -C tools/rmtool # builds rmtool + lib/librmapi.a
+make -C tools/rmtop # builds rmtop (ncursesw)
+make -C tools/rmq # unchanged, still builds
+```
+
+Requirements: gcc, ncursesw for rmtop. No external SDK, NVML or
+libnvidia-ml linkage. `make clean` removes all artifacts.
+
+## 4. Running
+
+```bash
+tools/rmtool/rmtool --help
+tools/rmtool/rmtool gpu list --format=json
+tools/rmtool/rmtool gpu info -d 0
+tools/rmtool/rmtool engine list
+tools/rmtool/rmtool mem stats
+tools/rmtool/rmtool sample timer --interval=100ms --count=20
+tools/rmtop/rmtop --once
+tools/rmtop/rmtop -g 0 --interval=250 # interactive (needs a TTY)
+```
+
+`gpu list`/`gpu topology` need no device access (they read
+`/proc/driver/nvidia/gpus` and PCI sysfs). Everything else opens a
+short-lived RM client and issues read-only controls; this is the same
+access model as `nvidia-smi` and is safe alongside a running CUDA job.
+
+`--device-spec`/`-d` accepts a minor (`0`) or a bus id (`0000:01:00.0`) and
+maps to the RM `deviceId`; `--device PATH` overrides the `/dev/nvidiaN`
+node.
+
+## 5. green-eye integration
+
+Wrappers `rmharness/rmtool.sh`, `rmharness/rmtop.sh`, `rmharness/rmq.sh`
+locate and build the tools and pass arguments through. `rmharness/harness.sh`
+adds capture:
+
+```bash
+cd debug/risc-v-gsp-rm-vm
+./rmharness/harness.sh --session dumps/real-rm-before --name before \
+ -- rmtool engine list --format=json
+./rmharness/harness.sh --session dumps/real-rm-before -- rmtop --once
+```
+
+The harness appends one `rm_harness_run` event per run and stores
+stdout/stderr under `<session>/blobs/` with sizes and SHA-256. Existing
+sessions are appended to, never rewritten; a missing `session.json` is
+created with the `greeneye-session/1` format. Because these runs are
+external, the event `t` field is `0.0`; `start`/`end` (epoch seconds) carry
+the wall clock. `greeneye analyze` tolerates the session; the run count shows
+up in the event summary.
+
+`plugins/rmharness_plugin.py` exposes the same wrappers in the REPL
+(`rmtool`, `rmtop`, `rmq`), recording through `ge.recorder.blob()` +
+`ge.recorder.event()`. Write-looking arguments are refused and recorded as
+`rm_harness_skip`. The plugin is stdlib-only and is exercised without QEMU
+by `rmharness/tests/test_rmharness_plugin.py` (fake `ge`).
+
+## 6. Safety contract (absolute)
+
+* Host-side read-only queries only. The tools allocate only an RM client /
+ device / subdevice handle pair; they never create engine, channel or memory
+ objects.
+* Never load/unload kernel modules, bind/unbind the driver, create/destroy
+ mdevs, claim the physical GPU, reboot, or touch `/sys/fs/pstore`.
+* Write paths are enforced in code, not by convention:
+ * `rmtool` refuses `mem alloc/map/unmap/pin`, `engine allocate/start/stop/
+ config`, and `channel/context/resserve/cudasw/resman/gsp` with exit 4/3;
+ * `nv2080 method call` only sends controls on a read-only allowlist
+ (`GPU_GET_INFO_V2`, `GPU_GET_NAME_STRING`, `GPU_GET_ENGINES_V2`,
+ `MC_GET_ARCH_INFO`, `FB_GET_INFO_V2`, `BUS_GET_PCI_INFO`,
+ `TIMER_GET_TIME`, `FIFO_GET_DEVICE_INFO_TABLE`,
+ `GPU_GET_CLASSLIST_V2`, ...) and refuses anything else before the ioctl;
+ * `harness.sh` only accepts `rmtool`, `rmtop`, `rmq`;
+ * the plugin blocks write-looking arguments.
+* Another agent may be running a live CUDA/PLAN B job; the tools do not
+ touch its context and all smoke checks above run without a GPU claim.
+
+## 7. What was and was not run
+
+Smoke checks executed on the development host (driver 570.211.01, TU104
+RTX 2080 Super Max-Q, one GPU at `0000:01:00.0`):
+
+* `--help`/`--version` for both tools; `gpu list`, `gpu topology` (proc/sysfs
+ only);
+* `gpu info`, `engine list`, `mem stats`, `sample timer`, `rmtop --once`
+ (read-only RM controls) — all returned live data (`TU100`, 10 engines GR +
+ 5 CE + NVDEC + NVENC + SW + SEC2, 8 GiB VRAM, BAR1 256 MiB);
+* `harness.sh` session recording and the plugin test.
+
+Not run, and why: `rmq` device info tables and `nv2080 class list` return
+`NV_ERR_INSUFFICIENT_PERMISSIONS` / `NV_ERR_NOT_SUPPORTED` for an
+unprivileged client on this driver (reported, not escalated); interactive
+`rmtop` was not driven (no TTY in the build shell); nothing here exercises
+migrations, MIG or a second GPU.
+
+## 8. Roadmap tie-ins
+
+* **docs/06-ACCURACY** — recorded `rm_harness_run` blobs are the host
+ reference for M1/M6; keep the JSON captures in the session directories
+ cited by the metric table.
+* **GREEN-EYE-ROADMAP H3** — the custom simulator's device map and RM-core
+ interception need real control semantics (IDs, sizes, status values) to
+ validate against; `rmtool` supplies that inventory before the H2.2 boot is
+ reached, and the shared `rm_api.c` can become the simulator's host-RM
+ client for differential tests.
+* **GREEN-EYE-ROADMAP H4.5** — the canonical observe -> compare -> implement
+ loop can now include a host capture (`harness.sh --session`) beside the
+ simulator session, giving every emulation change an evidence pair.
+* **R4 (full-memory trace)** — the session-event schema already carries
+ external runs; when the TCG trace lands, host `rm_harness_run` events and
+ firmware `access` events can be interleaved for a complete real-vs-sim
+ picture.
+* **H1.3** — injected services that emulate RM controls should be diffed
+ field-by-field against the `rmtool nv2080 method call` reply for the same
+ control id.
+
+## 9. Extending safely
+
+1. Add new read-only controls to the allowlist **and** to `rmtool.h`
+ structs, with the exact kernel-side size (the userspace SDK snapshot is
+ not authoritative).
+2. Never add an override that bypasses the allowlist/refusals.
+3. Record any new query in the session and document the skip reason for
+ anything privilege-gated.
+4. Keep `tools/rmq/` untouched; call it from the harness.
diff --git a/debug/risc-v-gsp-rm-vm/docs/18-HARNESS-AUTOMATION.md b/debug/risc-v-gsp-rm-vm/docs/18-HARNESS-AUTOMATION.md
new file mode 100644
index 0000000..ecd0614
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/18-HARNESS-AUTOMATION.md
@@ -0,0 +1,284 @@
+# 18 — Harness automation: portability matrix and mutation harness
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status.
+
+Two stdlib-only orchestration tools in `harness/` turn the manual playbooks
+into repeatable runs:
+
+| Tool | Roadmap | What it does |
+|---|---|---|
+| `harness/portability_matrix.py` | **R6**, H4.1 | Runs probe + scripted demo + recorded sessions against several firmware ELFs and emits a cross-target delta report with the M1–M6 skeleton. |
+| `harness/mutation_harness.py` | **R5**, H4.3 | Deterministically mutates the demo firmware's mailbox input surface, classifies abnormal outcomes, and records every attempt as a session plus a compact finding. |
+
+Both use the **QEMU backend only** (the same `probe`/`demo` machinery as the
+CLI). They intentionally do not depend on `greeneye.sim_backend`, and they
+never attach to an existing gdbstub, hardware, a guest or a kernel module.
+
+Requirements: `qemu-system-riscv64`, Python 3.10+, and (for the bundled demo)
+`make demo`. All commands run from `open-iov/debug/risc-v-gsp-rm-vm/`.
+
+---
+
+## 1. Portability matrix
+
+### 1.1 Usage
+
+Self-comparison of one build (a determinism check):
+
+```bash
+python3 harness/portability_matrix.py \
+ --target firmware/uproc_demo/build/uproc_demo.elf \
+ --target firmware/uproc_demo/build/uproc_demo.elf \
+ --out dumps/matrix-demo \
+ --probe-instructions 200 --scenario-duration 0.2
+```
+
+Old vs new firmware with the project's symbol DBs (one DB applies to every
+target in the invocation; run separate invocations for per-version DBs):
+
+```bash
+python3 harness/portability_matrix.py \
+ --target firmware/57X-GSP/570.144/gsp-570.144.fwimage.bin \
+ --target firmware/57X-GSP/570.211/gsp-570.211.fwimage.bin \
+ --ghidra-index analysis/570.144/decompiled_index.tsv --ghidra-bias 0 \
+ --symbols symbols-570.144.json \
+ --out dumps/matrix-570
+```
+
+Replacing the builtin scenario with a fixed green-eye script:
+
+```bash
+python3 harness/portability_matrix.py --target <rehosted.elf> \
+ --scenario-script examples/scripts/trace_rpc.py --out dumps/matrix-rehosted
+```
+
+Flags: `--probe-instructions N` (0 = wall-clock `--probe-duration`),
+`--scenario-duration`, `--max-stops`, `--max-rows`, `--symbols`,
+`--ghidra-names/--ghidra-index/--ghidra-exports/--ghidra-callgraph`
+(`--ghidra-bias`), and repeatable `--target` (positional targets also work).
+
+### 1.2 What each target does
+
+1. **Probe** — the ELF is flattened and relocated to `0x80000000` exactly as
+ `greeneye probe` does, then run for a fixed instruction budget (default
+ 2000) or a wall-clock budget. The first exception is parsed with
+ `greeneye.analyze.parse_qemu_faults`; the stopped PC is annotated from the
+ symbol DB. Raw `.fwimage` targets fault early here; that is the expected
+ first-touch data, not a harness failure.
+2. **Symbols** — ELF `.symtab` plus optional Ghidra exports and/or an
+ existing JSON DB, merged and saved to `<out>/symbols/<target>.json`
+ (runtime addresses; the relocation `bias` is recorded in the DB meta).
+3. **Scenario** — the target boots under QEMU with hooks on every `rpc_*`
+ handler that resolves and a write watchpoint on `gsp_ctrl.rpc_count`, runs
+ its self-test, then receives one deterministic host stimulus
+ (`mailbox = PING (1)`, `arg0 = 0x1234`). Everything is recorded as a
+ normal `greeneye-session/1` directory. A re-hosted ELF without `rpc_*`/
+ `gsp_ctrl` symbols still runs; it simply produces an empty profile.
+
+### 1.3 Output format
+
+```text
+<out>/
+ portability-report.json stable machine-readable report
+ portability-report.md rendered report
+ symbols/<target>.json merged symbol DB per target
+ sessions/<target>-probe/ recorded probe session
+ sessions/<target>-scenario/ recorded scripted session
+ compare/<a>--<b>.md greeneye compare report per target pair
+```
+
+JSON shape (stable keys, targets in CLI order, tables sorted):
+
+```json
+{
+ "format": "greeneye-portability-matrix/1",
+ "tool": "harness/portability_matrix.py",
+ "backend": "qemu",
+ "targets": [{
+ "name": "...", "path": "...", "sha256": "...", "entry": "0x...",
+ "status": "ok",
+ "probe": {"status": "...", "stopped_pc": 0, "stopped_symbol": "...",
+ "fault_count": 0, "first_fault": null},
+ "symbols": {"count": 13, "sources": ["elf"], "path": "symbols/x.json"},
+ "scenario": {"session": "sessions/x-scenario", "event_counts": {},
+ "bp": {}, "watch": {}, "writes": {}, "ctrl": {}},
+ "metrics": [{"id": "M1", "status": "not measured", "observed": {}}]
+ }],
+ "comparisons": [{"a": "...", "b": "...", "deltas": {"bp": [], "watch": [], "writes": []},
+ "all_zero": true, "metrics": []}],
+ "skipped": []
+}
+```
+
+Determinism: fixed probe instructions, fixed scenario phases and the same
+stimulus make hit/watch/write profiles reproducible, so an identical target
+compared with itself reports **zero deltas**. The M6 control-block snapshot
+hash is *not* a fixed point while the firmware heartbeats; compare snapshots
+only between quiesced runs. Metrics are marked `"not measured"` unless a
+reference capture exists (docs/06 §5); `observed` carries the local facts the
+matrix could gather.
+
+Targets that are missing, not RISC-V ELF, or too large are reported with
+`status: "skipped"` and a `skip_reason`; a failing step yields `"error"` for
+that step and does not abort the matrix.
+
+---
+
+## 2. Mutation harness
+
+### 2.1 Usage
+
+```bash
+python3 harness/mutation_harness.py --out dumps/mutation-7 \
+ --iterations 64 --seed 7 --timeout 0.25 --settle 0.2
+```
+
+Targeting the command/args surface plus an optional RAM window:
+
+```bash
+python3 harness/mutation_harness.py --out dumps/mutation-ram \
+ --iterations 128 --seed 11 --memory-region 0x80000900:0x100
+```
+
+Budgets: `--iterations` (1..10000), `--seed`, `--timeout` (0.01..10 s total
+observation per mutation; a short first window plus one confirmation window
+for slow handlers), `--settle` (per attempt, until `SELFTEST DONE` is seen),
+and `--max-log-bytes` (QEMU log window per fault check; trap loops flood
+`-d int`). `--kernel` defaults to the bundled demo; a non-demo ELF is refused
+unless `--allow-foreign-elf` is given, and remains QEMU-only.
+
+### 2.2 Strategies
+
+| Strategy | Input surface |
+|---|---|
+| `cmd-invalid` | command IDs outside the demo's valid range (`0, 7..15, 24+, 0x100, 0xffff, 2^64-1`) |
+| `arg-boundary` | one of arg0/arg1/arg2 set to `0, 1, 2, 0xff, 0x100, 0x7fffffff, 2^63, 2^64-1`, ... |
+| `bit-flip` | one bit flipped in the command or an argument of a valid command |
+| `buffer-length` | ECHO/FILL/SUM against `gsp_ctrl.buf` with lengths `0, 1, 31, 32, 33, 255, 256, 257, 4096, 2^32, ...` |
+| `mem-region` | one byte bit-flip inside `--memory-region` (when supplied) |
+
+Each iteration uses a **fresh QEMU session**, settles the demo through its
+self-test, applies exactly one mutation, runs bounded, then reads back the
+control block. No hardware watchpoints are installed, so the only stops are
+budget interrupts and real traps.
+
+### 2.3 Outcome classes
+
+| Class | Detection |
+|---|---|
+| `ok` | command dispatched (or `cmd=0`) and the demo oracle, when one exists, matched |
+| `trap` | first new exception is a breakpoint (`ebreak`), or control transferred below `0x100` |
+| `fault` | first new QEMU exception is a load/store/fetch/illegal fault (`parse_qemu_faults`) |
+| `no-progress` | command written but `rpc_count` did not advance within both windows (no fault) |
+| `unexpected-result` | result/last_rpc contradict the documented demo oracle for `cmd ∈ {0,1,3,5, invalid}` |
+| `exit` / `settle-failed` / `error` | target exited; self-test never observed; iteration exception |
+
+The oracle is deliberately small: handlers that dereference `arg0` (ECHO,
+SUM) and injected slots 0–5 have no oracle, so their successful executions
+are `ok` with `expected: null`.
+
+### 2.4 Output format
+
+```text
+<out>/
+ findings.jsonl one compact finding per iteration (deterministic order)
+ summary.md unique outcome classes, strategy coverage, examples, replay command
+ sessions/iter-0000/ recorded greeneye session per iteration
+```
+
+Finding shape:
+
+```json
+{"iteration": 5, "strategy": "arg-boundary",
+ "mutation": {"cmd": 4, "arg0": 0, "arg1": 255, "arg2": 0, "region": null},
+ "outcome": "fault",
+ "detail": {"observed": {"result": 48, "last_rpc": 5, "rpc_count_delta": 0},
+ "fault": {"cause": 5, "desc": "fault_load", "epc": 2147483860,
+ "tval": 255, "access": {"rw": "read", "access_addr": 255,
+ "size": 1, "reason": "rejected"}}},
+ "session": "sessions/iter-0005"}
+```
+
+Replay is re-running the same command: the per-iteration RNG is seeded from
+`(seed, index)`, finding fields avoid wall-clock data, and `findings.jsonl`
+is byte-identical for the same seed/budgets on the same host. Session
+directories under `sessions/` contain the usual `events.jsonl`, `session.json`
+and `summary.md`; they are the evidence behind each finding.
+
+---
+
+## 3. Tests
+
+```bash
+python3 -m unittest tests.test_harness -v
+make test # includes the harness tests with the rest of the suite
+```
+
+The integration tests boot the demo twice for a self-comparison (must show
+zero deltas) and run 12 seeded iterations twice (findings file, at least one
+outcome class, byte-identical replay). They skip cleanly when QEMU or the
+demo ELF is absent.
+
+---
+
+## 4. Attaching reports to a portability change
+
+For the docs/07 P1/P2/P3 loop and the docs/14 §6 evidence pack:
+
+1. Run one matrix invocation per version/device/architecture family with the
+ same scenario options, and keep `<out>/portability-report.json`, the
+ per-pair compare reports and the session directories (or their sha256
+ list).
+2. Record the firmware hash (`sha256` in each target entry), the symbol
+ source list and `bias` from `<out>/symbols/*.json` meta.
+3. Fill the M1–M6 table using `observed` from the matrix plus the real
+ reference capture; the matrix marks agreement metrics "not measured" on
+ purpose. Cite docs/06 L2/L3 limits in any claim.
+4. For an input-surface change, run the mutation harness before and after the
+ change and diff the outcome-class tables; attach `findings.jsonl` as the
+ corpus entry list, with each finding's session as evidence.
+5. Re-run the unchanged stimulus after the emulation change: profiles must
+ improve or stay equal (the docs/06 accuracy loop).
+
+---
+
+## 5. Safety
+
+* Demo / re-hosted firmware under QEMU only. Never physical hardware, never
+ a guest, never a kernel module, never an existing gdbstub or the native
+ simulator.
+* The mutation harness refuses non-demo ELFs unless `--allow-foreign-elf` is
+ passed explicitly, and still refuses non-ELF images.
+* Memory-region mutation is bounds-checked against the QEMU RAM window; no
+ host memory is touched outside the emulated target.
+* Every write is recorded in a session (`mem_write`, `note`, mutation fields);
+ budgets (`duration`/`max_stops`/`--timeout`) bound every run.
+* Only temporary work directories and `<out>/` are written on the host.
+
+---
+
+## 6. Limits and fidelity
+
+* L2/L3 claims are out of scope: no timing, interrupt or device-model
+ conclusions (docs/06). The matrix observes RAM-resident behaviour and the
+ first unmapped access, nothing more.
+* A QEMU log window is bounded; a fault not in the window is recorded as its
+ secondary signal (`trap_loop`/`no-progress`) rather than guessed.
+* Mutation covers one mailbox write per iteration; multi-write races and
+ concurrency are future work (H2/H4.4).
+* The demo oracle is small and documented; "unexpected-result" is only
+ meaningful for the listed commands.
+
+---
+
+## 7. Roadmap mapping
+
+| Item | Where this doc/tool contributes |
+|---|---|
+| **R5 — fuzzing and mutation harness** | `mutation_harness.py`: bounded seeded mutation, outcome classes, recorded corpus (`findings.jsonl` + sessions) |
+| **R6 — multi-version orchestration** | `portability_matrix.py`: probe + rehost/demo + compare + M1–M6 for a target set |
+| **H4.1 — semantic diffing** | identical stimulus per target, `compare_sessions` plus hit/watch/write deltas in one report |
+| **H4.3 — mutation/fuzzing** | error-path corpus with symbol-annotated sessions, feeding fail-closed rules |
+| **H4.5 — reimplementation loop** | reports are the attachable evidence for the docs/07 checklist |
+| **V1 — metrics/golden data** | deterministic profiles and seeded findings support golden comparisons |
+| **H7 — agentic harness** | self-describing JSON/markdown artifacts designed for an agent to drive and review |
diff --git a/debug/risc-v-gsp-rm-vm/docs/19-CONTROL-DISCOVERY.md b/debug/risc-v-gsp-rm-vm/docs/19-CONTROL-DISCOVERY.md
new file mode 100644
index 0000000..0f9afbe
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/19-CONTROL-DISCOVERY.md
@@ -0,0 +1,445 @@
+# 19 — Control discovery and hot-path reachability
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status and
+> [`GREEN-EYE-ROADMAP.md`](../GREEN-EYE-ROADMAP.md) H4 for the program.
+
+Two harness tools close the loop between "a new firmware image does something
+we do not model" and "here is the code path, its trigger and its effects":
+
+| Tool | What it answers |
+|---|---|
+| `harness/control_discovery.py` | Which command IDs exist, which code path each one takes, and what it changes. New/undiscovered controls fall out of the sweep instead of waiting for documentation. |
+| `harness/reach.py` | Given one decompiled symbol/address: is it reachable, what triggers it, what arguments does it need, what happens. |
+
+Both are simulator-only, bounded, evidence-producing, and depend only on the
+public `Session`/hook/watch primitives (stdlib-only Python). They are the
+practical start of the "Sandsifter for RM" idea in
+[`GREEN-EYE-ROADMAP.md`](../GREEN-EYE-ROADMAP.md) H4 and reuse the contracts
+in [06-ACCURACY](06-ACCURACY.md) and [14-ADVERSARIAL-INTEROP-GUIDE](14-ADVERSARIAL-INTEROP-GUIDE.md).
+
+---
+
+## 1. The problem
+
+The GSP's RM is a message/method system: services own objects and answer
+controls dispatched by ID. NVIDIA ships new images on a hidden schedule,
+stripped and signed, and new services, messages and controls appear between
+versions. Static diffing (docs/18 portability matrix, docs/14 section 4.1)
+tells us the code changed; it does not tell us which input activates the new
+path, what the path needs, or what it changes. A sweep of the input surface
+plus dynamic hooks answers exactly those questions, and it is safe to run in a
+simulator where the firmware cannot be damaged.
+
+## 2. The discovery model
+
+### 2.1 Sweep spaces
+
+A "call surface" is a finite input space with an observable effect:
+
+| Surface (today / future) | Space | Probe action |
+|---|---|---|
+| demo mailbox (`--surface demo`) | command IDs `1..--max-id`, with argument patterns | write `gsp_ctrl.arg0..2`, then `gsp_ctrl.mailbox` |
+| RM control/method entry (`--surface rm`, H1.3) | control IDs, classes and method opcodes | call the RM method entry with `(control, a0, a1, a2)` |
+| future CSB/TRANSCOM message functions | message function IDs | write the message buffer and ring the doorbell |
+
+ID 0 is the demo's idle mailbox sentinel (the polling loop ignores it), so the
+sweep starts at 1; surface classes with a meaningful 0 can start there instead.
+
+The same machinery applies to any of them because the sweep is described by a
+:class:`Surface` subclass: a probe function, an effect observer, and an
+entry-point table.
+
+### 2.2 Deterministic argument patterns
+
+Every ID is probed with `--patterns` (default all three):
+
+* `zero` — `(0, 0, 0)` (the "no arguments" baseline);
+* `sentinel` — three distinct pseudo-random 64-bit values from a stream keyed
+ by `(seed, id)`, so an ID that misuses an argument is caught;
+* `boundary` — `(2^64-1, 1, 2^63-1)`, the usual off-by-one/size boundaries.
+
+Patterns are deterministic: the same `--seed`/`--max-id` replays the same
+findings byte-for-byte (verified by `tests/test_discovery.py`).
+
+### 2.3 Entry-point modelling
+
+Before probing, the tool reads the surface's declared entry points:
+
+* the dispatcher symbol (`rpc_dispatch` on the demo) is hooked for every
+ probe, capturing the command and the raw `arg0..arg2` at dispatch entry;
+* every non-zero handler-table slot (`gsp_ctrl.handlers[0..7]`) is hooked so a
+ hit can be attributed to a function even when the command is not one of the
+ documented switch cases.
+
+On real RM the same role is played by the RM-core dispatch table / object
+class table: hook the dispatcher, then resolve the handler the simulator
+routes to.
+
+### 2.4 Effect capture
+
+Each probe observes:
+
+* control-block fields before/after (`state`, `rpc_count`, `last_rpc`,
+ `result`, `buf` hash) plus write watchpoints on the scalar fields, which
+ also record the *store PC* (`effects.watch`);
+* the new QEMU exception window (`-d guest_errors,unimp,int`), parsed with
+ `greeneye.analyze.parse_qemu_faults`, so traps (`cause 3`) and faults are
+ distinguished from "no progress";
+* stop signals as a backend-independent fallback (a native backend has no
+ QEMU log: SIGTRAP maps to trap, SIGSEGV/SIGBUS/SIGILL to fault; budget
+ SIGINTs are deliberately excluded from findings).
+
+### 2.5 Classification
+
+One probe yields one outcome, in priority order:
+
+| Outcome | Meaning |
+|---|---|
+| `trap` | breakpoint exception (`ebreak`) or trap loop below `0x100` |
+| `fault` | load/store/fetch/illegal exception |
+| `method-return` | an RM-style call entry returned a value |
+| `no-progress` | the mailbox was never consumed in the budget |
+| `injected-slot` | command routed through the injected-service handler table |
+| `known-handler` | a declared handler ran |
+| `default-path` | the dispatcher catch-all ran (no dedicated handler) |
+| `settle-failed` / `error` | the pre-probe state or the probe itself failed |
+
+`default-path`, `injected-slot` and `trap/fault` are the interesting classes:
+they are where new behaviour, injection wiring and input bugs live.
+
+---
+
+## 3. `control_discovery.py` — unknown-call discovery
+
+### 3.1 Usage
+
+Full demo mailbox sweep (189 probes: IDs 1..63 times three patterns):
+
+```bash
+python3 harness/control_discovery.py --out dumps/discovery --seed 7
+```
+
+A quick sweep for CI or bring-up (IDs 1..32, one pattern):
+
+```bash
+python3 harness/control_discovery.py --surface demo --out dumps/discovery-min \
+ --max-id 32 --patterns zero --seed 5 --timeout 0.25
+```
+
+Other surfaces / budgets:
+
+```bash
+# RM surface (documented future interface; see section 5)
+python3 harness/control_discovery.py --surface rm --out dumps/discovery-rm
+
+# cap the probe count and keep every recorded session
+python3 harness/control_discovery.py --out dumps/discovery-cap --iterations 40 --record-all
+```
+
+Flags: `--max-id` (1..4096), `--patterns zero,sentinel,boundary`, `--seed`,
+`--iterations` (0 = all probes), `--timeout` (per-probe observation window),
+`--settle` (self-test budget), `--backend qemu|native` (QEMU is the supported
+default; native drops the QEMU fault log and uses signals), `--kernel`
+(bundled demo by default; a foreign ELF needs `--allow-foreign-elf`),
+`--record-classes` and `--record-all`, `--max-log-bytes`.
+
+Each probe uses a **fresh session**, settles through the self-test, then
+applies one command. Fresh sessions are the safe strategy: a trap or fault can
+leave the demo in a state that would corrupt the next probe.
+
+### 3.2 Output
+
+```text
+<out>/
+ findings.jsonl one compact finding per probe (deterministic order)
+ discovery-report.json format + options + per-ID aggregate + findings
+ control-discovery.md rendered report grouped by class
+ sessions/<idx>-id<NNN>-<pattern>/ kept evidence for interesting classes
+```
+
+Finding shape (stable keys, no wall-clock data):
+
+```json
+{"probe": 15, "cmd": 16, "pattern": "zero", "args": [0, 0, 0],
+ "outcome": "injected-slot",
+ "handler": "rpc_ping", "handler_entry": 2147483720, "documented_handler": null,
+ "dispatch_seen": true, "dispatch_pc": 2147483912, "dispatch_args": [16, 0, 0, 0],
+ "rpc_count_delta": 1,
+ "effects": {"changed": [{"field": "result", "before": 48, "after": 48879}],
+ "watch": [{"field": "result", "pc": 2147484066, "symbol": "rpc_dispatch"}]},
+ "fault": null, "signals": [], "settle_ok": true,
+ "session": "sessions/0015-id016-zero", "reason": null}
+```
+
+### 3.3 Reading the report
+
+The markdown report opens with the outcome counts and an "Ids by class" line,
+then sections that matter:
+
+* **Known handler ids** — the documented dispatch table (by the firmware's own
+ control block, not the docs);
+* **Injected-slot dispatch ids** — commands whose path is the injected-service
+ table, with the handler each aliases;
+* **Unknown / undiscovered ids** — the explicit list that answers "what is
+ not handled here yet";
+* **Traps and faults** — IDs that reach error paths under some input;
+* **All findings by id** — the join key for `findings.jsonl`.
+
+### 3.4 What the demo sweep already shows
+
+Measured on 2026-10-02, demo ELF sha256 `c038a04a...`, 189 probes in ~36 s:
+
+* IDs 1..6 hit `rpc_ping`/`rpc_echo`/`rpc_fill`/`rpc_sum`/`rpc_state`/
+ `rpc_fault`; the ID 6 path ends in the `ebreak` trap and never writes
+ `result` (the dispatcher's post-handler stores do not run);
+* **discovered class:** IDs 16..23 fall through the switch into the
+ injected-service handler table. IDs 16..21 alias the *built-in* handlers
+ (`16 -> rpc_ping`, ..., `21 -> rpc_fault`); IDs 22..23 are NULL slots and
+ return `~id`. This is exactly the kind of wiring a new firmware can add
+ silently;
+* IDs 7..15 and 24..63 take the catch-all and return `~id` with
+ `rpc_count + 1` — the unknown-ID space;
+* argument patterns change the class where the handler uses the argument:
+ `rpc_echo` and `rpc_sum` fault on sentinel/boundary pointers, while
+ `rpc_ping`/`rpc_state`/`rpc_fill` stay on the handler path.
+
+---
+
+## 4. `reach.py` — hot-path reachability
+
+### 4.1 Usage
+
+```bash
+# Directly callable target, with an argument
+python3 harness/reach.py --target rpc_ping --method call --args 0x41 \
+ --out dumps/reach-ping
+
+# Natural boot/self-test path
+python3 harness/reach.py --target rpc_sum --method run --out dumps/reach-sum
+
+# Only reachable through a mailbox command: discover the trigger from the
+# handler table and apply it
+python3 harness/reach.py --target rpc_fault --method trigger --out dumps/reach-fault
+
+# Replay a saved probe spec; emit one for CI
+python3 harness/reach.py --probe dumps/reach-fault/probe.json --emit-probe /tmp/fault-probe.json
+```
+
+Flags: `--method auto|stimulus|trigger|run|call`, `--all-methods`, `--args`,
+`--arg-count` with `--arg-fill zero|sentinel`, `--timeout`, `--settle`,
+`--stimulus script.py`, `--probe spec.json`, `--emit-probe`, `--record [DIR]`,
+`--max-prior`, `--kernel`, `--backend`, `--allow-foreign-elf`.
+
+### 4.2 Methods
+
+`auto` tries methods in this order and stops at the first hit (use
+`--all-methods` to compare routes):
+
+1. `stimulus` — run a green-eye script (or a probe spec's stimulus) with the
+ target hook and watchpoints armed. The script drives `session.run` itself.
+2. `trigger` — read the surface's handler table, find the command ID(s) that
+ resolve to the target, settle the firmware, apply that command and run
+ bounded. This is the trigger discovery for IDs 1..6 and 16..23 on the demo.
+3. `run` — `Session.run_until(target, timeout)` from launch, which catches the
+ natural boot/self-test path.
+4. `call` — `Session.call(target, args)`, the Frida-like direct call, for
+ unit-testing a function in isolation.
+
+A method reports a hit when the target-entry hook fires, even if execution
+immediately traps (`rpc_fault`): reachability and success are different
+questions, and the report distinguishes them.
+
+### 4.3 Output
+
+```text
+<out>/
+ reach-report.json machine-readable report (stable keys)
+ reach-report.md rendered report
+ probe.json suggested repeatable probe spec (on a hit)
+ sessions/<method>/ optional recorded session (--record)
+```
+
+The report records:
+
+* `reachable`, `method`, and every `methods_tried` with its miss reason;
+* `entry`: the register state at the target (args `a0..a3`, `ra`, `sp`) and
+ `dispatch_args` — the authoritative command arguments when the target was
+ reached through the dispatcher;
+* `prior_events`: the ordered hook events that led to the hit (bounded by
+ `--max-prior`);
+* `required_args`: discovered arguments plus their provenance (`call-args` or
+ `firmware-dispatch`) and the caller `ra`;
+* `effects`: field deltas, watchpoint stores, the return value, and whether a
+ trap/fault followed (`rpc_fault`: trap `ebreak` at `pc 0`);
+* `trace_window`: hook counts and, when recorded, session event counts;
+* `probe_spec`: the exact spec to replay the route.
+
+### 4.4 Demo results
+
+Measured on 2026-10-02 (each run ~0.5 s, QEMU):
+
+| Target | Method | Result |
+|---|---|---|
+| `rpc_ping` | `call` with `--args 0x41` | reachable; returns `0x41 ^ 0xbeef = 0xbeae` |
+| `rpc_sum` | `run` | reachable in the self-test; entry args `(buf, 16, 32, ...)`; path passes through `rpc_ping` -> `rpc_echo` -> `rpc_fill` first |
+| `rpc_fault` | `trigger` | reachable via mailbox command 6 (also aliased at 21); dispatch args `cmd=6, a0=0, a1=0`; immediate `ebreak` trap |
+
+The self-test path is a useful reminder that compile-time inlining can bypass
+the dispatcher: the `run` route reaches `rpc_sum` from an inlined call site,
+so there are no `rpc_dispatch` hits before it. The `trigger` route does pass
+through the dispatcher and exposes the control arguments.
+
+---
+
+## 5. Sandsifter for RM (the future surface)
+
+A Sandsifter-style tool for RM needs three more capabilities than the demo
+has today, in this order.
+
+### 5.1 The `Surface` abstraction (already in place)
+
+`harness/control_discovery.Surface` separates:
+
+* `probe(session, id, args)` — how one call is applied (mailbox write today;
+ RM method call / message write later);
+* `observer()` — the effect model (control-block fields today; RM reply
+ buffer and object state later);
+* `handler_map()` / `command_for_addr()` — how IDs map to code, which drives
+ both discovery and reach's `trigger` method.
+
+Adding a real RM surface is a new subclass, not a rewrite of the sweep,
+classification or reporting.
+
+### 5.2 RM sweep design
+
+1. **Control/method inventory.** Start from `rmtool`/`rmq` (docs/17) for the
+ host-visible control list and class IDs; import the Ghidra RM-core dispatch
+ table into the symbol DB. The sweep ranges are then control IDs by class
+ (`--rm-class`) and method opcodes within a control, mirroring the
+ `nv2080 method call` surface.
+2. **Entry-point modelling.** Hook the RM dispatch routine, the object
+ constructor/method tables and the status/notifier writes. For each swept
+ ID, capture the handler entry, the object it selects and the status it
+ returns.
+3. **Effect observation.** Point `--rm-reply` at the reply buffer for the
+ control and hash/snapshot it; add watchpoints on the object handle table
+ and status queue. Compare the reply field-by-field against the host oracle
+ (`rmtool` JSON) for M2 (docs/06).
+4. **Branch exploration.** The native simulator's snapshots (H3) make it cheap
+ to fork at the dispatcher and sweep IDs without re-booting; the QEMU
+ surface uses fresh sessions exactly like today.
+5. **Corpus and gates.** Findings become an inventory corpus (`findings.jsonl`)
+ that CI diffs between firmware versions; new IDs, new handlers, or changed
+ default-path results are the regression signal (docs/21).
+
+### 5.3 Rollout gates
+
+* Entry gate: H2.2/H1.3 — a simulator session reaches the RM message loop and
+ exposes the dispatch table (otherwise `--surface rm` exits cleanly with the
+ message below).
+* Exit gate: sweep a real control space, reach one previously unknown control,
+ and replay its probe spec through `reach.py` with a recorded session.
+
+When unavailable the tool is explicit, not mysterious:
+
+```text
+$ python3 harness/control_discovery.py --surface rm --out dumps/discovery-rm
+[discovery] skipped: surface 'rm' unavailable: no RM control/method entry point is exposed
+by the simulator or a live session yet (roadmap H1.3). Pass --rm-entry SYMBOL once the
+RM method dispatcher exists; the demo surface works today with --surface demo.
+```
+
+---
+
+## 6. Feeding the simulator/device model and CI
+
+* **Device model.** Each discovered class is a candidate row in the
+ simulator's dispatch/control model: the command ID, the entry symbol, the
+ argument pattern that reaches it, and the effect snapshot are exactly the
+ data the H2/H3 device map needs. Unknown IDs (`default-path`) document the
+ fail-closed/default behaviour the emulation must reproduce.
+* **Accuracy metrics.** The `dispatch_pc`/`handler_entry`/effects fields feed
+ M1 (inventory coverage) and M4/M6 (state and memory side effects,
+ docs/06); compare two firmware versions by diffing `discovery-report.json`
+ (new handler IDs, moved handler entries, changed default results).
+* **CI (docs/21).** The intended gates are:
+ 1. `control_discovery.py --max-id 32 --patterns zero` as a fast inventory
+ smoke test: known handlers must resolve, `rpc_fault` must trap, and the
+ default-path result must stay `~id`;
+ 2. `reach.py` on a pinned list of targets, asserting `reachable` and the
+ method (`call` for `rpc_ping`, `run` for `rpc_sum`, `trigger` for
+ `rpc_fault`) so a compiler/inlining or dispatch change is caught;
+ 3. diff `findings.jsonl`/`discovery-report.json` against a golden run.
+ Findings contain no wall-clock data, so the diff is stable for a fixed
+ seed/params (the same property the mutation harness documents in docs/18).
+* **Probe specs are the interface.** `reach.py --emit-probe` and
+ `--probe` turn a discovered route into an artifact that any agent or CI job
+ can replay without re-deriving it.
+
+---
+
+## 7. Tests
+
+```bash
+python3 -m unittest tests.test_discovery -v
+make test # includes the discovery tests with the rest of the suite
+```
+
+`tests/test_discovery.py` covers the pure functions (pattern determinism,
+probe ordering/caps, classification priority), a 32-probe demo sweep that
+must attribute IDs 1..6, find an unknown ID and an injected-slot alias and
+write the report, byte-identical replay for a fixed seed, the clean RM skip,
+and the three reach demonstrations (`rpc_ping` call, `rpc_sum` run,
+`rpc_fault` trigger). It skips cleanly when QEMU or the demo ELF is absent.
+The suite completes in well under a minute.
+
+---
+
+## 8. Safety and ethics
+
+* **Simulator only.** The tools launch their own QEMU (or the native
+ simulator with `--backend native`) against the bundled demo or an
+ explicitly allowed re-hosted ELF. They never attach to hardware, a guest, a
+ kernel module or an existing gdbstub. Ctrl-C leaves no target running: each
+ session is closed in a `finally`.
+* **Bounded probes.** Every probe has a settle budget and an observation
+ window; `--max-id`/`--iterations` cap the sweep; QEMU log reads are capped.
+* **No hardware writes.** All memory writes go through the debug stub into
+ emulated RAM; the only host writes are under `--out`.
+* **Evidence, not truth.** A `default-path` result is evidence about this
+ firmware at this fidelity (L0/L1, docs/06), not a statement about silicon.
+ Never use a simulator observation to bypass a hardware protection; escalate
+ anything that would change the product to a physical capture (docs/14
+ sections 6-7).
+* **No injected payloads here.** Discovery only *observes* injection slots; it
+ never loads a blob. Injection itself stays behind `greeneye inject` and the
+ docs/12 contract.
+
+---
+
+## 9. Limits and fidelity
+
+* QEMU wall-clock slicing bounds when a handler is interrupted, so a very long
+ handler may be classified from its entry hit without its final result; the
+ finding says `known-handler` and leaves `effects` empty rather than guessing.
+* The demo is not RM: there is no object model, no status queue and no
+ per-control reply. The RM design in section 5 is deliberately unbuilt until
+ the simulator exposes an RM dispatch table (H1.3/H2.2).
+* `trap` vs `fault` uses the guest exception cause; QEMU and the native
+ simulator agree on the demo, but x86/RISC-V mapping differences are a
+ documented fidelity gap (docs/06 section 8).
+* The sweep is single-threaded and deterministic; concurrent/multi-core
+ dispatch interleavings (H2.4) are future work.
+
+---
+
+## 10. Roadmap mapping
+
+| Item | Contribution |
+|---|---|
+| **H4.adversarial navigation** | unknown-call discovery and trigger discovery over control surfaces |
+| **H4.1 semantic diffing** | `discovery-report.json` is a version-diffable inventory (new/changed IDs, handlers, default paths) |
+| **H4.2 capability inference** | `effects.watch` store PCs are access evidence for the capability table |
+| **H4.5 reimplementation loop** | `reach.py` output plus probe spec is the observe -> reproduce artifact for a new behaviour |
+| **H3 native simulator** | `--backend native` reuses the same sweep once parity lands; snapshots will remove the fresh-session cost |
+| **V1 golden data / docs/21 CI** | stable findings and the fast 32-probe gate are the golden corpus |
diff --git a/debug/risc-v-gsp-rm-vm/docs/20-MCP-SERVER.md b/debug/risc-v-gsp-rm-vm/docs/20-MCP-SERVER.md
new file mode 100644
index 0000000..a3b6111
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/20-MCP-SERVER.md
@@ -0,0 +1,277 @@
+# 20 — MCP server (green-eye agent interface)
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status.
+
+## 1. What it is
+
+`plugins/mcp_server.py` is the green-eye **MCP (Model Context Protocol)
+server** — roadmap [`R3`](08-ROADMAP.md#r3-mcp-server-plugin) and
+[`H7`](../GREEN-EYE-ROADMAP.md). It exposes the *active* `Session` as MCP
+tools so an AI agent (Claude Code, opencode, any MCP client) can inspect
+symbols, set breakpoints/watchpoints, run the target, read memory, snapshot,
+analyse/compare recordings, inject services, and drive the harnesses.
+
+* **Stdlib-only**: JSON-RPC 2.0 over newline-delimited stdin/stdout, no MCP
+ library, no network listener unless explicitly asked.
+* **Plugin first**: `register(ge)` adds an `mcp-serve` REPL/CLI command that
+ serves the session already loaded by green-eye.
+* **Standalone too**: the same file runs as a script that launches its own
+ session (`--kernel`, `--backend`, `--record`) and then serves MCP.
+* **Read-only by default**: memory writes, remote calls and injection require
+ explicit startup flags.
+* **Defensive capabilities**: tools owned by other components (mutation and
+ portability harnesses, native trace, injection) return a structured
+ `not_available` tool error when the module/file is absent instead of
+ crashing. The tool list always advertises every capability.
+
+The core green-eye package is untouched; everything lives in the plugin.
+
+## 2. Running it
+
+All commands run from `open-iov/debug/risc-v-gsp-rm-vm/`.
+
+### 2.1 Standalone (the usual MCP client mode)
+
+```bash
+# bundled demo, QEMU, recorded session
+python3 plugins/mcp_server.py --stdio --record
+
+# native simulator, recorded
+python3 plugins/mcp_server.py --stdio --backend native --kernel firmware/uproc_demo/build/uproc_demo.elf --record
+
+# enable writes / injection (never on by default)
+python3 plugins/mcp_server.py --stdio --allow-write --allow-inject
+
+# local loopback TCP instead of stdio (for clients that cannot spawn a process)
+python3 plugins/mcp_server.py --tcp 127.0.0.1:8765 --record
+```
+
+Flags: `--stdio` (default), `--tcp HOST:PORT` (loopback only),
+`--kernel ELF`, `--backend qemu|native`, `--record [PATH]`, `--name NAME`,
+`--plugins PATH` (repeatable), `--allow-write`, `--allow-inject`.
+Logs go to stderr; **stdout carries protocol frames only**.
+
+### 2.2 Plugin mode (serve a live session)
+
+```bash
+./greeneye.sh attach --kernel firmware/uproc_demo/build/uproc_demo.elf \
+ --plugin plugins/mcp_server.py
+greeneye> mcp-serve # read/observe
+greeneye> mcp-serve --allow-write # + memory writes / remote calls
+greeneye> mcp-serve --allow-inject # + service injection
+```
+
+`mcp-serve` blocks until the MCP client disconnects or calls the `close`
+tool, then returns to the REPL. In plugin mode `close` stops the server but
+**does not** close the session (the REPL still owns it); in standalone mode
+`close` also closes the launched session.
+
+Both `--stdio` and `--tcp` behave identically. The bundled plugin directory is
+loaded by default, so `mcp_server.py` is auto-discovered; the command only
+serves when invoked.
+
+## 3. Client configuration
+
+The MCP server must be started **before** the session it should serve, so
+client configs use standalone mode. Use absolute paths.
+
+### 3.1 Claude Code / `.mcp.json`
+
+```json
+{
+ "mcpServers": {
+ "green-eye": {
+ "command": "python3",
+ "args": [
+ "/home/rmcore/Git/integdev_gpu_drv/open-iov/debug/risc-v-gsp-rm-vm/plugins/mcp_server.py",
+ "--stdio",
+ "--backend", "native",
+ "--kernel", "/home/rmcore/Git/integdev_gpu_drv/open-iov/debug/risc-v-gsp-rm-vm/firmware/uproc_demo/build/uproc_demo.elf",
+ "--record"
+ ]
+ }
+ }
+}
+```
+
+Or, one line: `claude mcp add green-eye -- python3
+<tool-root>/plugins/mcp_server.py --stdio --record`.
+
+### 3.2 opencode (`opencode.json`)
+
+```json
+{
+ "$schema": "https://opencode.ai/config.json",
+ "mcp": {
+ "green-eye": {
+ "type": "local",
+ "command": [
+ "python3",
+ "/home/rmcore/Git/integdev_gpu_drv/open-iov/debug/risc-v-gsp-rm-vm/plugins/mcp_server.py",
+ "--stdio", "--backend", "native", "--record"
+ ],
+ "enabled": true
+ }
+ }
+}
+```
+
+For an agent that must write memory or inject, add `--allow-write` /
+`--allow-inject` to `args` and keep the session recorded (the recording is
+the audit trail).
+
+## 4. Protocol
+
+| Request | Behaviour |
+|---|---|
+| `initialize` | returns `protocolVersion` (echoes `2024-11-05`, `2025-03-26` or `2025-06-18`; otherwise the server default), `serverInfo`, `capabilities.tools` |
+| `notifications/initialized` | accepted silently (notifications never get a response) |
+| `ping` | `{}` |
+| `tools/list` | the 24-tool catalogue with JSON input schemas |
+| `tools/call` | `{"name": ..., "arguments": {...}}` → `{"content":[{"type":"text","text":"<json>"}]}`, or `isError: true` for tool errors |
+
+JSON-RPC errors: `-32700` parse error, `-32600` invalid request, `-32601`
+unknown method, `-32602` unknown tool / bad params. Tool-level failures
+(bad address, symbol not found, gating, capability absent) are returned as
+`isError: true` results whose text is a JSON object with `error` and
+`message` — the server never dies on a bad call.
+
+Tool result payloads are always a single JSON text block so agents can parse
+them deterministically.
+
+## 5. Tool catalogue
+
+| Tool | Arguments | Class | Notes |
+|---|---|---|---|
+| `session_info` | — | read | backend, PC, symbols, recording, breakpoints/watches, enabled capabilities |
+| `symbols_resolve` | `target` | read | symbol, `symbol+0xNN` or hex address |
+| `bp_set` | `target`, `kind=sw\|hw` | observe | hits recorded as `bp_hit` |
+| `bp_clear` | `target` | observe | removes every breakpoint at that address |
+| `watch_set` | `target`, `size=8`, `kind=w\|r\|a` | observe | hits recorded as `watch_hit` |
+| `run` | `duration=1.0`, `max_stops`, `timeout` | observe | bounded; services hooks/watchpoints |
+| `run_until` | `target`, `timeout=5.0` | observe | temporary breakpoint |
+| `step` | `count=1` (max 10000) | observe | dispatches hooks/watchpoints |
+| `read_mem` | `addr`, `size=64` (max 1 MiB) | read | lowercase hex |
+| `write_mem` | `addr`, `hex` | **write** | requires `--allow-write`; recorded as `mem_write` |
+| `hexdump` | `addr`, `size=64` (max 1 MiB) | read | symbol-annotated text |
+| `snapshot` | `addr`, `size` (max 16 MiB), `label=mcp` | read | zlib + sha256 into the session |
+| `call_function` | `target`, `args=[]`, `timeout=5.0` | **write** | requires `--allow-write`; Frida-like remote call |
+| `inject_service` | `service`, `slot=6`, `load_addr`, `cmd=1`, `arg0`, `arg1`, `path=remote\|firmware\|both`, `settle=0.25` | **inject** | requires `--allow-inject`; service ELF built with `make sdk` |
+| `trace_start` | `exec=true`, `start`, `end`, `max_events` | trace | native backend + recording session only |
+| `trace_stop` | — | trace | flushes `blobs/trace.jsonl`, returns counts |
+| `analyze_current` | `max_rows=40` | read | `render_summary` for the active recording |
+| `compare_sessions` | `a`, `b`, `symbols=true`, `max_rows=40` | read | A/B recorded sessions |
+| `export_hits` | `session`, `out` | read | Ghidra `greeneye_hits.tsv` |
+| `probe_firmware` | `elf`, `instructions=2000`, `duration=1.0`, `no_relocate`, `symbols`, `timeout=300` | harness | QEMU-only probe subprocess; first divergence |
+| `mutation_run` | `kernel`, `out`, `iterations=10`, `seed=1`, `timeout=0.25`, `settle=0.2`, `memory_region`, `allow_foreign_elf` | harness | `harness/mutation_harness.py`, QEMU-only |
+| `portability_matrix_run` | `targets[]`, `out`, `probe_instructions=2000`, `scenario_duration=0.4`, `symbols`, `max_rows` | harness | `harness/portability_matrix.py`, QEMU-only |
+| `plugins_list` | — | read | bundled plugins, `GREENEYE_PLUGINS`, REPL commands |
+| `close` | — | session | stop serving; standalone also closes the session |
+
+Capability classification follows the manifest in
+[15-AGENTIC-GSP-IDE](15-AGENTIC-GSP-IDE.md) §5: read / observe are
+unattended-safe; write and inject require the explicit startup flags and a
+recorded session. `bp_set`, `watch_set`, `run` and `step` mutate target
+*observation state* only and stay in the observe class.
+
+Harness and probe tools shell out to the existing CLIs with bounded budgets
+and return their stdout/stderr; they never touch hardware, a guest or a
+kernel module.
+
+## 6. Example agent sessions
+
+### 6.1 Monitor an automated firmware-release run
+
+```json
+{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"session_info","arguments":{}}}
+{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"symbols_resolve","arguments":{"target":"rpc_ping"}}}
+{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"bp_set","arguments":{"target":"rpc_ping"}}}
+{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"run","arguments":{"duration":0.5,"max_stops":100}}}
+{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"analyze_current","arguments":{"max_rows":20}}}
+```
+
+The agent watches `bp_hit`/`watch_hit` counts in the report, snapshots the
+control block, runs the release stimulus, then diffs the new recording
+against the golden one:
+
+```json
+{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"snapshot","arguments":{"addr":"gsp_ctrl","size":392,"label":"release"}}}
+{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"compare_sessions","arguments":{"a":"dumps/golden","b":"dumps/release","symbols":true}}}
+```
+
+### 6.2 Dump memory
+
+```json
+{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"hexdump","arguments":{"addr":"0x80000840","size":128}}}
+{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"read_mem","arguments":{"addr":"0x80000840","size":16}}}
+{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"snapshot","arguments":{"addr":"0x80000000","size":4096,"label":"ram-window"}}}
+```
+
+### 6.3 Inject a service
+
+Start the server with `--allow-write --allow-inject --record`, then:
+
+```json
+{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"inject_service","arguments":{"service":"sdk/build/injected_demo.elf","slot":6,"cmd":1,"arg0":65,"arg1":66,"path":"both"}}}
+```
+
+The result contains the load address/entry and both the direct (`remote`) and
+firmware-mailbox (`firmware`) return values; every segment write and call is
+recorded, and the session can be compared like any other run.
+
+## 7. Security rules
+
+1. **Local only.** Default transport is stdin/stdout. `--tcp` binds loopback
+ addresses only (`127.0.0.1`, `localhost`, `::1`); any other host is
+ refused at argument parse time. There is no auth, so never expose it.
+2. **No guest, no hardware.** The server drives the simulator/QEMU session
+ only. It does not touch `/dev`, a kernel module, an mdev, a guest or a
+ physical GPU; the agent runs against the simulator by default
+ ([H7](../GREEN-EYE-ROADMAP.md), [15-AGENTIC-GSP-IDE](15-AGENTIC-GSP-IDE.md) §5).
+3. **Read-only by default.** `--allow-write` enables `write_mem` and
+ `call_function`; `--allow-inject` enables `inject_service`. The agent's
+ client config decides which manifest class it gets; a read/observe agent is
+ configured without those flags.
+4. **Sessions are the audit trail.** Writes, calls, injections and snapshots
+ land in `events.jsonl`; run with `--record` (the trace tools require it).
+ A finding without a session is not a finding.
+5. **Budgets everywhere.** `run` defaults to 1 s, `step` is capped, reads and
+ snapshots have size caps, and harness subprocesses have timeouts. A wedged
+ target is left stopped and restarted clean, never poked.
+6. **Protocol hygiene.** stdout is reserved for JSON-RPC; diagnostics go to
+ stderr. One bad tool call returns `isError` and the server keeps serving.
+7. **No product changes.** MCP can propose evidence, not merge emulation
+ changes; human approval still gates `open-iov-vfio`.
+
+## 8. Absent capabilities
+
+`probe_firmware`, `mutation_run`, `portability_matrix_run`, `trace_*`,
+`inject_service`, `analysis` and the Ghidra export are checked at call time.
+If a module/file is missing the tool returns:
+
+```json
+{"content":[{"type":"text","text":"{\"error\": \"not_available\", \"message\": \"mutation harness not present: ...\"}"}],"isError": true}
+```
+
+so an agent can plan around a capability instead of seeing the server crash.
+The tool list is stable regardless of what is present.
+
+## 9. Testing
+
+```bash
+python3 -m unittest tests.test_mcp -v # 34 tests, no external deps
+```
+
+The suite drives `initialize`/`tools/list`/`tools/call` over in-memory
+streams (including unknown method/tool, parse errors, write/inject gating,
+no-stdout-pollution and structured `not_available`), plus a standalone
+subprocess test on the native backend that skips cleanly when the native
+simulator is absent.
+
+## 10. Roadmap
+
+* **R3 (done)** — MCP plugin exposing sessions as agent tools; local stdio,
+ no network by default; capability gating.
+* **H7 remaining** — named harness entry points and budgets, a session index
+ for agents, enforced capability manifest files, and the physical-escalation
+ gate. Those build on this server, not around it.
diff --git a/debug/risc-v-gsp-rm-vm/docs/21-CI-RELEASE-PIPELINE.md b/debug/risc-v-gsp-rm-vm/docs/21-CI-RELEASE-PIPELINE.md
new file mode 100644
index 0000000..d687cca
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/21-CI-RELEASE-PIPELINE.md
@@ -0,0 +1,320 @@
+# 21 — CI and release pipeline (version diff + evidence bundle)
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status.
+
+## 1. Purpose
+
+This is the release-triage path for a new GSP firmware image (roadmap R6 /
+H4.1, docs/23 C5/C7). Two stdlib-only tools turn the manual docs/07 P1/P2
+playbook into one repeatable run and one attachable bundle:
+
+| Tool | What it does |
+|---|---|
+| `harness/version_diff.py` | old vs new target, section by section: image/shape, symbol inventory, decompiled corpus hints, bounded QEMU probe, shared-scenario behavior, M1–M6 skeleton |
+| `harness/ci_release.py` | runs version_diff → portability matrix → mutation smoke and hashes every artifact into `bundle-manifest.json` |
+
+`.github/workflows/greeneye.yml` runs the tool's test suite on pushes/PRs that
+touch `debug/risc-v-gsp-rm-vm/**` or `tools/rm*/**`. The repository-level
+`.github/workflows/ci.yml` is independent and unchanged.
+
+Everything here is local: the tools never download firmware, never attach to
+hardware, a guest or a kernel module, and use their own QEMU processes.
+
+---
+
+## 2. Version diff
+
+### 2.1 Usage
+
+Self-diff of the demo (a determinism/regression check; must be all-zero):
+
+```bash
+python3 harness/version_diff.py \
+ --old firmware/uproc_demo/build/uproc_demo.elf \
+ --new firmware/uproc_demo/build/uproc_demo.elf \
+ --out dumps/vdiff-selfcheck \
+ --probe-instructions 200 --behavior-duration 0.2
+```
+
+Old vs new firmware with per-version symbol DBs and Ghidra exports:
+
+```bash
+python3 harness/version_diff.py \
+ --old firmware/57X-GSP/570.144/gsp-570.144.fwimage.bin \
+ --new firmware/57X-GSP/570.211/gsp-570.211.fwimage.bin \
+ --old-label 570.144 --new-label 570.211 \
+ --old-ghidra-exports analysis/570.144/symbols.tsv \
+ --new-ghidra-exports analysis/570.211/symbols.tsv \
+ --corpus-diff analysis/570.144/decompiled_index.tsv analysis/570.211/decompiled_index.tsv \
+ --out dumps/vdiff-570
+```
+
+Attaching a fixed stimulus and a pre-produced corpus report:
+
+```bash
+python3 harness/version_diff.py --old <rehosted-old.elf> --new <rehosted-new.elf> \
+ --stimulus examples/scripts/trace_rpc.py \
+ --corpus-report run/corpus-delta-570.144-570.211.txt \
+ --out dumps/vdiff-rehosted
+```
+
+Targets are RISC-V ELF files or raw images (`.fwimage`). A raw target is
+relocated to `0x80000000` as one implicit load; its firmware-vaddr translation
+is `runtime - 0x80000000`. Symbols may be absent for raw images; the symbol
+section then reports `skipped` with a reason unless Ghidra/JSON sources are
+supplied.
+
+Flags: `--old/--new` (required), `--out` (required), `--old-label/--new-label/
+--release`, `--old-symbols/--new-symbols` (green-eye JSON), `--old-ghidra-names`,
+`--new-ghidra-names`, `--old-ghidra-index`, `--new-ghidra-index`,
+`--old-ghidra-exports`, `--new-ghidra-exports` (`--old-ghidra-bias`/
+`--new-ghidra-bias`), `--corpus-diff OLD NEW`, `--corpus-report FILE`,
+`--stimulus`, `--probe-instructions` (`0` = `--probe-duration` wall clock),
+`--behavior-duration`, `--max-stops`, `--max-rows`, `--approval-token`.
+
+### 2.2 Sections
+
+Every section reports `status: ok | skipped | error`, and a skipped section
+never aborts the others. A failing QEMU probe or scenario is recorded as
+`error` for that section only.
+
+1. **Image / shape.** Entry, span, file size/hash, PT_LOAD layout (vaddr,
+ filesz/memsz, flags), and the ELF section table (name/type/addr/size).
+ Raw images have no section table; that is stated, not guessed.
+2. **Symbol inventory.** ELF `.symtab` plus Ghidra `names.tsv`,
+ `decompiled_index.tsv`/bucket TSVs, 4-column `exports.tsv` and the
+ 3-column `ExportForGreenEye` form (`addr<TAB>name<TAB>size`), plus
+ green-eye `greeneye-symbols/1` JSON DBs. Matching follows
+ `userspace/cuda/tools/diff_corpus.py`: named functions match by name
+ (duplicates in address order), the address shift is the median of named
+ deltas, and `FUN_*` functions match at the shifted address. The report
+ lists added/removed/moved/resized (named and anonymous), the shift and its
+ dominance, match rates and the named-transfer rate. Merged per-side DBs are
+ saved under `<out>/symbols/`.
+3. **Decompiled-change hints.** `--corpus-diff A B` runs the same matching
+ discipline over two corpus indexes and writes `<out>/corpus/corpus-diff.json`;
+ `--corpus-report FILE` attaches a previously produced report (hash +
+ preview). With neither flag the section is `skipped` with reason
+ `not supplied: ...`.
+4. **Probe first-touch.** Both targets are flattened, relocated to
+ `0x80000000`, stepped for the bounded budget, and their QEMU logs parsed
+ with `greeneye.analyze.parse_qemu_faults`. The first fault is reported with
+ cause/desc, `epc` **and** `epc_firmware`, the access address **and**
+ `access_firmware` (translated only when the access address falls inside the
+ image). The diff says whether the stopped PC/first fault changed.
+5. **Behavior.** The shared demo scenario from `portability_matrix` (hooks on
+ every resolvable `rpc_*`, a write watchpoint on `gsp_ctrl.rpc_count`, the
+ self-test, one deterministic PING) or a `--stimulus` script is run on both
+ targets and recorded as normal `greeneye-session/1` directories. The report
+ includes `analyze.compare_sessions` output under `<out>/compare/`, explicit
+ hit/watch/written-page deltas, `all_zero`, and the docs/06 M1–M6 skeleton
+ with every agreement metric marked `not measured` (a reference capture is
+ required, docs/06 §5).
+
+### 2.3 Output
+
+```text
+<out>/
+ version-diff.json stable machine-readable report
+ version-diff.md rendered report
+ symbols/<target>.json merged symbol DB per side (when non-empty)
+ sessions/<target>-probe/ recorded probe session
+ sessions/<target>-behavior/ recorded scenario session
+ compare/<old>--<new>.md compare_sessions report
+ corpus/corpus-diff.json when --corpus-diff is used
+```
+
+Exit status: `0` for a normal report (skips included), `2` for a tool error,
+`3` for an RM 610 gate refusal.
+
+---
+
+## 3. RM 610 gate (hard, testable)
+
+The gate is enforced by `version_diff` and `ci_release` **before any target
+file is opened** and before `--out` is created:
+
+* a target/release label matching `rm610` / `RM-610` / `rm_610` etc. (the
+ `--release`, `--old-label`, `--new-label` values and the target basenames)
+ is refused unless
+ * `--approval-token RM610-APPROVED` is passed, or
+ * `GREENEYE_RM610_APPROVED=1` is set;
+* the refusal text points at [`docs/23-RM610-READINESS.md`](23-RM610-READINESS.md)
+ and states that nothing was downloaded and no target file was opened;
+* the same check runs in `--dry-run` mode.
+
+Nothing in either tool performs network access, and `--dry-run` never touches
+a target. Current-version rehearsal runs use `570.144`-labelled inputs; 610
+inputs require the §6 approval message in docs/23.
+
+---
+
+## 4. Release pipeline
+
+### 4.1 Dry run first
+
+```bash
+python3 harness/ci_release.py --old <old> --new <new> --out dumps/release-570 \
+ --old-label 570.144 --new-label 570.211 --dry-run
+```
+
+`--dry-run` prints a deterministic JSON plan (the three step commands and the
+artifact paths) and exits `0` without creating `--out` or opening a target.
+This is the docs/23 §4 step 6 rehearsal.
+
+### 4.2 Real run
+
+```bash
+python3 harness/ci_release.py --old <old> --new <new> --out dumps/release-570 \
+ --old-label 570.144 --new-label 570.211 \
+ --old-symbols symbols-570.144.json --new-symbols symbols-570.211.json \
+ --stimulus examples/scripts/trace_rpc.py \
+ --mutation-kernel firmware/uproc_demo/build/uproc_demo.elf \
+ --mutation-iterations 8 --mutation-seed 7
+```
+
+Steps, in order:
+
+1. **version_diff** — writes `<out>/version-diff/` (section flags above);
+2. **portability_matrix** — old and new as two targets, same budgets, writes
+ `<out>/portability-matrix/portability-report.json|.md` and per-pair compare
+ reports; the self-comparison must show `all_zero`;
+3. **mutation_smoke** — `mutation_harness` on the demo (or `--mutation-kernel`,
+ with `--allow-foreign-mutation` for a re-hosted ELF) with a small budget;
+ writes `<out>/mutation-smoke/findings.jsonl|summary.md`.
+
+A step that raises is recorded as `status: error` and the later steps still
+run; the process exits `1` when any step errored, `0` otherwise (`skipped`
+steps do not fail the run, but their reasons are in the report). Exit `3` is
+the RM 610 refusal.
+
+`ci_release` flags: the `version_diff` inputs above plus `--matrix-symbols`
+(one JSON DB applied to both matrix targets), `--mutation-kernel` (default:
+the bundled demo), `--mutation-iterations`/`--mutation-seed`/
+`--mutation-timeout`/`--mutation-settle`, and `--allow-foreign-mutation` for a
+non-demo re-hosted ELF.
+
+### 4.3 Evidence bundle format
+
+```text
+<out>/
+ release-report.json pipeline summary (steps, gate, labels)
+ bundle-manifest.json sha256 + byte size of every other file
+ version-diff/ docs/21 §2.3 tree
+ portability-matrix/ portability-report.json|.md, symbols/, sessions/, compare/
+ mutation-smoke/ findings.jsonl, summary.md, sessions/
+```
+
+`bundle-manifest.json`:
+
+```json
+{
+ "format": "greeneye-release-bundle/1",
+ "tool": "harness/ci_release.py",
+ "files": [{"path": "version-diff/version-diff.json", "bytes": 12345, "sha256": "..."}]
+}
+```
+
+`release-report.json` shape:
+
+```json
+{
+ "format": "greeneye-ci-release/1",
+ "dry_run": false,
+ "rm610_gate": {"status": "passed", "labels": ["570.144", "570.211"]},
+ "old": {"label": "570.144", "path": "gsp-570.144.fwimage.bin"},
+ "new": {"label": "570.211", "path": "gsp-570.211.fwimage.bin"},
+ "steps": [{"name": "version_diff", "status": "ok",
+ "sections": {"image": "ok", "symbols": "ok", "corpus": "ok",
+ "probe": "ok", "behavior": "ok"}}],
+ "errors": [],
+ "manifest": "bundle-manifest.json"
+}
+```
+
+The manifest is written last, so its hashes cover the final release report.
+Attach the directory (or its manifest) to the docs/07 evidence checklist and
+the docs/14 §6 pack; the docs/23 §4 step 7 expected-artifact list maps 1:1.
+
+---
+
+## 5. CI workflow
+
+`.github/workflows/greeneye.yml` (new; `ci.yml` is untouched):
+
+* triggers on push/PR paths `debug/risc-v-gsp-rm-vm/**` and `tools/rm*/**`;
+* `ubuntu-latest`, no secrets, no hardware;
+* installs `qemu-system-misc clang lld-18`;
+* builds `make -C debug/risc-v-gsp-rm-vm sdk demo`;
+* runs `python3 -m unittest discover -s tests -t .` from the tool root (QEMU
+ integration tests run; if QEMU were absent they skip cleanly).
+
+The intended release gates this workflow covers are the fast inventory smoke
+from docs/19 §6 (`control_discovery --max-id 32 --patterns zero`), the harness
+suites, and the docs/21 dry-run. A full `ci_release` run against real firmware
+is driven manually/agentically because it needs the versioned inputs and
+symbol DBs; CI-adjacent automation should call it with `--dry-run` first.
+
+---
+
+## 6. MCP monitoring
+
+`plugins/mcp_server.py` (docs/20) exposes the same machinery to an agent:
+`probe_firmware`, `portability_matrix_run`, `mutation_run`, `compare_sessions`
+and `analyze_current`. The agent pattern for a release:
+
+1. `portability_matrix_run` with the old/new targets and fixed budgets;
+2. `mutation_run` on the demo with the pinned seed and budget;
+3. `compare_sessions` for the recorded evidence pair;
+4. read `version-diff.json` / `release-report.json` / `bundle-manifest.json`
+ from the run directory (agent reads the bundle, it does not recompute it).
+
+The MCP server remains read/observe by default and simulator-only (docs/20 §7).
+
+---
+
+## 7. Determinism, limits and safety
+
+* Fixed probe budgets, fixed scenario phases and the same stimulus make the
+ self-diff zero: an unchanged target reports zero symbol/probe/behavior
+ deltas (verified by `tests/test_version_diff.py`).
+* Agreement metrics M1–M6 are **not measured** without a reference capture
+ (docs/06 §5); the skeleton carries local `observed` facts only. L2/L3 claims
+ (timing, interrupts, device model) are out of scope.
+* Raw `.fwimage` targets execute as flat blobs; a firmware linked differently
+ may fault earlier than on silicon — that is first-touch data, not a boot
+ (docs/06 §3).
+* Only temporary work directories and `<out>/` are written; QEMU is always
+ launched and killed by the tools; log reads are bounded
+ (`--log-bytes`).
+* No downloads, no `/dev`, no kernel module, no existing gdbstub, no physical
+ GPU. The 610 gate is part of both CLIs, not documentation-only.
+
+---
+
+## 8. Tests
+
+```bash
+python3 -m unittest tests.test_version_diff -v
+make test # includes these with the rest of the suite
+```
+
+`tests/test_version_diff.py` covers the gate (default refusal, token/env
+approval, gate-before-open), the `diff_corpus.py` matching discipline
+(moved/resized/added/removed, anonymous shift), the 3-column export parser,
+missing/incompatible/raw target handling, corpus hints, `ci_release --dry-run`
+side-effect freedom, clean `--help`, and a demo self-diff asserting zero
+inventory and behavior deltas. QEMU integration tests skip cleanly when QEMU
+or the demo ELF is absent.
+
+---
+
+## 9. Roadmap mapping
+
+| Item | Where this doc/tool contributes |
+|---|---|
+| **R6 — multi-version orchestration** | `version_diff` + `ci_release` prefix the matrix with a version-scoped diff |
+| **H4.1 — semantic diffing** | symbol/corpus inventory deltas and behavior profiles for two firmware versions |
+| **V1 — metrics/golden data** | deterministic self-diff and stable JSON reports support golden comparisons |
+| **docs/23 C5/C7** | release pipeline implemented and exercisable; dry-run rehearsal path |
+| **H7 — agentic harness** | self-describing bundle + docs/20 MCP tools for monitored runs |
diff --git a/debug/risc-v-gsp-rm-vm/docs/22-PORTING-CUDA-AND-DEVICES.md b/debug/risc-v-gsp-rm-vm/docs/22-PORTING-CUDA-AND-DEVICES.md
new file mode 100644
index 0000000..30f4720
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/22-PORTING-CUDA-AND-DEVICES.md
@@ -0,0 +1,353 @@
+# 22 — Porting with green-eye: CUDA userspace, GSP firmware and new GPU devices
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00-OVERVIEW for status.
+> This is the combined porting guide for the three churn axes the project
+> faces. It joins the existing CUDA/GSP/firmware procedures with green-eye's
+> runtime layer. Items marked "planned (docs/xx)" do not exist yet; do not
+> invent commands for them.
+
+## 1. The three churn axes and the anchor discipline
+
+NVIDIA ships three things on independent schedules, and any one of them can
+break `open-iov-vfio` while the other two stay still:
+
+| Axis | Examples | Primary evidence path |
+|---|---|---|
+| **CUDA userspace version** (`libcuda`) | 570.144 → 570.211.01; future 580/610 families | CUDA corpus tooling + guest smoke (`userspace/cuda/docs/07`) |
+| **GSP firmware / RM version** | 570.144 → 570.211 GSP image; RM dispatcher and control-set changes | `greeneye probe` + `compare`, host oracle (`docs/05-operations/02` §4.1) |
+| **GPU device / architecture** | TU104 → GB202 (RTX 5090), GB10 (Grace-Blackwell), aarch64 hosts | Device profile + pairing check + portability matrix (docs/07 P2/P3, docs/18) |
+
+They interact:
+
+* a userspace bump usually implies an emulation-anchor decision (guest driver
+ and emulated firmware move together) even when the firmware binary is
+ byte-identical;
+* a device change can drag a different firmware family (TU10x vs GB100 vs
+ `gb10`) and a different partition/boot model (FMC/MP-SK; Init → ACR → RM for
+ confidential compute) with it;
+* an architecture change (aarch64 host, newer NV-RISCV core) can invalidate
+ environment assumptions that the other two axes assume are stable
+ (PMU/SOE provisioning, coherent memory, ISA extensions).
+
+**Anchor discipline.** The lock-step that must stay coherent is the
+**guest driver ↔ emulated firmware profile** (the GSP generation whose protocol
+the host emulates). The host's distribution driver is **not** part of that
+lock-step: it is the oracle used for A/B comparisons, and the optional OpenRM
+host modules are only for native vGPU-management workflows. The current anchor
+is **570.144**; a version bump moves the guest driver and the emulation, not
+the host distro driver (see `docs/05-operations/02-debugging-and-maintenance-workflows.md`
+§4.1). Porting rules that follow:
+
+1. One anchor per port; never evaluate a new `libcuda` against an old emulated
+ firmware profile and call a mismatch an emulation bug.
+2. Every input is versioned and hashed: `firmware/<SERIES>-GSP/<VER>/`,
+ `userspace/cuda/<version>/`, `config/versions/<MAJOR>.yaml`. The `config/`
+ YAML files are **design targets, not live configuration** until the module
+ load path reads them (`docs/06-portability/hardware-software-matrix.md` §6).
+3. New versions live in parallel directories; the previous anchor stays
+ untouched so rollback never destroys a working setup.
+4. A port is a claim with evidence attached (M1–M6, §4), not a merged patch.
+
+## 2. Joint workflow A — CUDA userspace version port (`libcuda` bump)
+
+### 2.1 Decide what kind of port it is (minutes)
+
+Run the cheap triage before any decompilation or emulation work:
+
+```bash
+cd userspace/cuda
+tools/cudatool.py info <new>/run/libcuda.so.<new>
+tools/cudatool.py delta <old>/run/libcuda.so.<old> <new>/run/libcuda.so.<new> > <new>/run/delta.txt
+```
+
+| `delta` reading | Port class | What it implies |
+|---|---|---|
+| no export moves, small `.text` growth | patch | transfer the name map and spot-check touched subsystems |
+| new/removed exports | feature | decode the new API surface before touching the emulation |
+| many moves | reshuffle (toolchain/linker) | plan a full re-analysis; a fraction of names will not transfer |
+
+### 2.2 Static analysis vs dynamic confirmation
+
+| Step | Existing project tooling | green-eye addition |
+|---|---|---|
+| Export/interface diff | `tools/cudatool.py delta` | — |
+| Function matching / name-transfer measurement | `tools/cudatool.py fdematch --names <names.tsv>` | — |
+| Corpus-level diff (added/removed/resized functions) | `tools/diff_corpus.py <old_index> <new_index>` | — |
+| Name transfer + corpus annotation | `ApplyNameMap.java` (`--dry-run` first), `tools/annotate_corpus.py` | `./greeneye.sh symbols export` builds a runtime-address symbol DB from the same index/names |
+| Driver-API smoke | `tools/test/cuda_drv_test.c` compiled as `/tmp/cdt` (prints `ALL OK`) | `./greeneye.sh symbols resolve --db ... <addr>` spot-checks transferred addresses per call path |
+| Runtime workload | `tools/cuda-ab/ab.sh all` (`vector_add.cu`; `RESULT`/`STATUS` must match host) | session-format evidence and `./greeneye.sh compare` when firmware-side sessions exist |
+| Guest-side control observation | `tools/guest-diag/ioctl_trace.c`, host module trace (`gsp-resman-vfio:`) | annotation of the firmware-side RPC path with the per-version symbol DB |
+| Firmware-side confirmation | — | `./greeneye.sh probe` / `record` / `compare` against the paired firmware |
+
+**Be honest about the execution boundary.** `cdt` and `vectorAdd` are x86_64
+guest binaries; the RISC-V simulator does not execute them. green-eye's
+contribution to a userspace port is the **symbol/evidence layer**: a
+runtime-address DB per `libcuda` version, per-address resolution checks for
+transferred names, and a recorded comparison format for the GSP-side behaviour
+the driver drives. The print-and-verify path (`cdt`, `ab.sh`, ioctl trace)
+remains the dynamic oracle.
+
+### 2.3 Step-by-step runbook
+
+1. **Acquire and version.** Fetch the new `libcuda.so.<version>`
+ (`570.144/scripts/fetch_from_guest.sh`, or the driver package with
+ `--extract-only`). Work in a new `userspace/cuda/<new>/` tree copied from
+ `570.144/`; never overwrite an analysed version.
+2. **Cheap triage.** `cudatool.py info` + `delta`; classify the port (§2.1);
+ record `.text` growth, build-id and ABI note.
+3. **Measure transfer before copying.** Run `cudatool.py fdematch` with the
+ old `names.tsv`. The known result from 570.144 → 570.211.01: all 659
+ exports transfer exactly, verified internals 5/7, wrapper-callee internals
+ 21/29, but only **~1% of generated `wrapper_*` addresses** transfer. Never
+ move wrapper names by address arithmetic; re-resolve them with
+ `cudatool.py wrappers` and re-run `annotate_corpus.py`.
+4. **Full analysis when needed.** For a feature/reshuffle port run
+ `ghidra_full_analysis.sh`, `ghidra_export_index.sh`,
+ `ghidra_batch_decompile.sh`, `ghidra_index_merge.sh` per
+ `userspace/cuda/docs/07-version-upgrade-playbook.md` §2, then
+ `diff_corpus.py` for the measured corpus delta.
+5. **Transfer names and knowledge.** Apply the map with `ApplyNameMap.java`
+ `--dry-run` first, then for real; translate the subsystem index; re-validate
+ entries the delta marked as moved.
+6. **Build the green-eye symbol DB** for the new version so runtime addresses
+ resolve:
+
+ ```bash
+ cd debug/risc-v-gsp-rm-vm
+ ./greeneye.sh symbols export \
+ --names ../../userspace/cuda/<new>/annotations/names.tsv \
+ --index ../../userspace/cuda/<new>/run/analysis/decompiled/decompiled_index.tsv \
+ --callgraph ../../userspace/cuda/<new>/run/analysis/elf-index/callgraph.tsv \
+ --bias 0x100000 -o symbols-<new>.json
+ ./greeneye.sh symbols resolve --db symbols-<new>.json 0x<transferred-addr>
+ ```
+
+ `--bias 0x100000` is the Ghidra image base convention for `libcuda`
+ (docs/02 §6); use `0` for firmware linked at its load address.
+7. **Dynamic confirmation on the emulated stack.** Compile and run `cdt`
+ (`tools/test/cuda_drv_test.c`), with the PTRACE ioctl tracer attached, on
+ the new userspace version against the **same** emulated firmware profile:
+ every `CHECK(...)` line and the final `ALL OK` is a per-entry-point
+ assertion (`cuInit`, `cuDeviceGetCount`, `cuDeviceGet`, `cuCtxCreate`,
+ `cuMemAlloc`, `cuMemcpyHtoD`, `cuMemcpyDtoH`, `cuMemFree`, `cuCtxDestroy`).
+ Then run the runtime workload end-to-end:
+
+ ```bash
+ tools/cuda-ab/ab.sh all # host oracle + guest run + compare
+ ```
+
+ `DEVICE` lines are normalised; `RESULT`/`STATUS` must match exactly. Any
+ new or reordered control in the ioctl trace is the interesting delta.
+8. **Compare across versions.** Diff `delta.txt`, the `fdematch` report, the
+ corpus diff, and the ioctl/control sequences old vs new. Where the firmware
+ side is recorded, use the session compare primitive:
+
+ ```bash
+ ./greeneye.sh compare dumps/<old-fw-run> dumps/<new-fw-run> --symbols
+ ```
+
+ For multi-target orchestration use the portability matrix (docs/18); note
+ it targets firmware ELFs, not `libcuda`.
+9. **Only then touch the emulation** — and only if the behaviour diff requires
+ it, with the docs/07 P4 escalation rule: any change that would change
+ emulation behaviour is reproduced against the physical/vfio-mdev reference
+ first.
+10. **Update documents**: the per-version decompilation map, `names.tsv` /
+ `subsystems.tsv`, the version-upgrade playbook notes, and the interface
+ doc only if controls/capability bits changed.
+
+### 2.4 Acceptance evidence (CUDA userspace port)
+
+* [ ] `delta` report (exports added/removed/moved, `.text` growth, build-id);
+* [ ] `fdematch` transfer table with counts per source class;
+* [ ] wrapper re-resolution evidence; no wrapper transferred by address;
+* [ ] `ApplyNameMap.java --dry-run` then real run; corpus diff report;
+* [ ] `symbols-<version>.json` with provenance/bias and spot-check resolutions;
+* [ ] `cdt` output ending `ALL OK`, with the ioctl trace attached;
+* [ ] `ab.sh` host/guest comparison with `RESULT`/`STATUS` matched;
+* [ ] ioctl/control diff old vs new explained line by line;
+* [ ] firmware anchor unchanged (or explicitly changed in the same PR);
+* [ ] M1–M6 table when firmware-side behaviour moved (§4);
+* [ ] docs updated; unexplained-delta list empty (empty is a claim).
+
+## 3. Joint workflow B — new GPU device (RTX 5090 / GB10-class)
+
+### 3.1 Device profile collection
+
+| Field | Where it comes from | Artifact |
+|---|---|---|
+| PCI IDs | `lspci`, vendor docs, `config/hardware/*.yaml` (design target) | device profile |
+| BAR layout / sizes | guest `lspci -vvv`, host sysfs | device profile |
+| FB geometry (VRAM, BAR1) | host oracle `rmtool mem stats --format=json` | capture in session |
+| Engine inventory | host oracle `rmtool engine list --format=json`, guest `guest_engines.c` | inventory table |
+| Interrupt topology | MSI-X tables, guest `lspci -vvv` | profile notes |
+| ROM/VBIOS quirks | ROM read, laptop/Surface variants | profile overrides |
+| Probe-time register values | guest driver init trace (`gsp-resman-vfio:`) | expected-value list |
+
+The host oracle (`rmharness`, docs/17) is read-only and safe alongside a live
+CUDA job; capture it into a session so the numbers carry SHA-256 provenance:
+
+```bash
+cd debug/risc-v-gsp-rm-vm
+./rmharness/harness.sh --session dumps/real-<device> --name before \
+ -- rmtool engine list --format=json
+./rmharness/harness.sh --session dumps/real-<device> -- rmtool mem stats --format=json
+```
+
+### 3.2 Firmware pairing first
+
+Verify the image family (TU10x / GH100 / LS10 / `gb10`), the FMC partition
+manifest and the version string **before** debugging anything. A mismatched
+pairing presents as an emulation bug — the project learned this with 570.144
+vs 570.211. Record firmware hash, family, version string and pairing notes at
+the top of the evidence pack.
+
+### 3.3 Probe first-touch diff and symbol inventory
+
+```bash
+./greeneye.sh symbols export --index <new-export.tsv> --bias 0 -o symbols-<new>.json
+./greeneye.sh probe <new-fwimage> --symbols symbols-<new>.json --record
+./greeneye.sh probe <reference-fwimage> --symbols symbols-<ref>.json --record
+```
+
+The probe loads the segments, runs bounded and reports the first divergence
+(faulting instruction, access address/size, stopped PC annotated from the
+symbol DB). Raw `.fwimage` targets fault early by design — that first-touch
+map is the data, not a failure. Changed first-touch addresses across devices
+usually point at profile data (register windows, boot args), not logic
+(docs/07 P2.2). The symbol inventory is the merged ELF/Ghidra DB per target;
+provenance (export file, bias) is recorded in the DB meta.
+
+### 3.4 Device-model implications (H2/H5)
+
+What the probe reveals feeds the roadmap, not the product directly:
+
+* **H2 (platform device model)** — falcon/SEC2 windows, CSB/TRANSCOM mailboxes,
+ WPR2/FWSEC read-back state, PTIMER, PMU stub, DMA/IOMMU. A device that
+ touches a different first register set changes the modelled register map
+ before the image can reach its message loop.
+* **H5 (adjacent microcontrollers)** — PMU/SEC2/ACR/SOE mailbox protocols and
+ boot ordering; a new device family may change identity/clock provisioning or
+ the Init → ACR → RM boot sequence.
+* Entry gate for device-model work is the portability task itself (docs/08 R1);
+ until then use probe + rehosted uproc paths.
+
+### 3.5 Folding into the hardware-software matrix
+
+The project's support ladder is `docs/06-portability/hardware-software-matrix.md`
+(Reference / Supported / Experimental / Target / Out of scope). A device is
+"tested and documented", not "should work". The fold-in:
+
+1. Run `harness/portability_matrix.py` across the reference and candidate
+ device firmware with the same scenario options (docs/18 §1); keep
+ `<out>/portability-report.json`, per-pair compare reports and sessions.
+2. The planned version-diff/CI pipeline (docs/21-CI-RELEASE-PIPELINE.md,
+ planned) is where those reports become release-gated artifacts; until it
+ exists, attach them to the matrix process manually.
+3. Update the device profile (documentation today; `config/hardware/*.yaml` is
+ not read by code — do not claim configuration-driven portability).
+4. Validate on the physical device with the supervised harness before claiming
+ support; simulator-only evidence never justifies the claim (docs/06 §6).
+
+### 3.6 aarch64 host / GB10-class design notes
+
+* **Host tooling** — green-eye is Python + QEMU; on aarch64 install
+ `qemu-system-misc` (provides `qemu-system-riscv64`) and the same commands
+ run. Keep host-specific paths out of session metadata (docs/07 P3.5).
+ `GREENEYE_QEMU` is the existing escape hatch if the binary lives elsewhere.
+* **Firmware family** — GB10-class devices pair a different firmware family
+ (`gb10`/GB100-era) than TU104's `57X-GSP`; treat the fwimage, FMC manifest
+ and PMU/SOE images as a new family, not a re-badged TU10x image.
+* **Boot/partition model** — expect the newer FMC/manifest partition model and,
+ for confidential-compute paths, the Init → ACR → RM flow; the rehost shim
+ needs the matching entry/return convention (docs/09 §4, docs/07 P3).
+* **PMU/SOE deltas** — identity and clock provisioning flow through PMU/SOE;
+ any change there invalidates prior environment assumptions and must be
+ recorded before probe results are interpreted.
+* **ISA/core** — confirm the image is still RV64IMAC or has moved to RV64GC
+ (`file`/`readelf`); update rehost `-march` flags accordingly. QEMU supports
+ RV64GC even where the bundled demo uses IMAC.
+* **Memory model** — Grace/GB10 coherence and page-size/cache-attribute
+ differences are memory-model risks (hardware-software-matrix §8); test on
+ aarch64 early rather than assuming x86 results transfer.
+
+## 4. Evidence and metrics contract for any port
+
+The metric contract is `docs/06-ACCURACY.md` §5; it is authoritative for every
+port type:
+
+| # | Metric | How it is measured in a port |
+|---|---|---|
+| M1 | Inventory coverage | observed controls/RPCs vs emulated handled set; host oracle inventory |
+| M2 | Reply agreement | field-by-field reply diff, canonical-size and consumed-field checks |
+| M3 | Ordering agreement | sequence alignment of the control/event stream (LCS/edit distance) |
+| M4 | State agreement | firmware state transitions vs the emulation state model |
+| M5 | Error-path agreement | injected faults take the same status/notifier path |
+| M6 | Memory side-effects | read/write map vs shadow state; snapshot hashes at fixed points |
+| M7 | Timing | **not claimed**; order-of-events budgets only |
+
+Rules: at least an A/B pair per conclusion; every claim carries its fidelity
+level (L0–L3) and provenance; a simulator observation is evidence about the
+firmware, not silicon; any product change is confirmed against a physical
+capture first. The evidence pack is docs/07's checklist plus docs/14 §6.
+
+**Checklist per port type**
+
+CUDA userspace (A):
+* [ ] §2.4 items complete; firmware anchor documented.
+
+GSP firmware version (P1):
+* [ ] firmware hash + version string + pairing notes;
+* [ ] symbol DB hash and Ghidra export provenance;
+* [ ] probe first-touch old vs new, every changed address explained;
+* [ ] portability matrix report + per-pair `compare`;
+* [ ] M1–M6 table with reference-capture method and L2/L3 limits;
+* [ ] existing guest ladder has no new regression;
+* [ ] every new/changed control has a version-keyed handler or explicit
+ "not used" note.
+
+Device (P2):
+* [ ] profile fields (§3.1) recorded with host-oracle captures;
+* [ ] pairing verified before any debugging;
+* [ ] probe first-touch diff explained as profile vs logic;
+* [ ] engine inventory complete and validated against the host oracle;
+* [ ] matrix entry and evidence published; physical validation done before the
+ support claim.
+
+Architecture (P3):
+* [ ] ISA/image shape confirmed (`file`/`readelf`), rehost flags updated;
+* [ ] partition/boot model identified; shim convention matched;
+* [ ] PMU/SOE deltas recorded; host portability verified on aarch64;
+* [ ] all P2 items.
+
+## 5. Common failure modes and decided responses
+
+| Failure mode | Signature | Decided response | Source |
+|---|---|---|---|
+| Mismatched guest driver ↔ firmware pairing | failures indistinguishable from emulation bugs (e.g. 570.144 image vs 570.211 driver) | verify hash/version string/pairing before debugging; fix the pairing, not the emulation | docs/07 P2.2; docs/05-operations §4.1 |
+| Runfile/metapackage version drift | a different major resolves than requested | check the candidate version; prefer the exact-version runfile | docs/05-operations §4.1 pitfalls |
+| Silent control-shape change | message layout changes break the emulator with no error | M2 canonical-size + consumed-field checks; per-control probes; decode against the exact-version headers | docs/05-operations §4.1 table |
+| Engine-inventory change blocking context creation | `cuCtxCreate` fails because an expected engine (e.g. `COPY14`) is absent | complete the inventory per device/driver profile, validate against `rmtool engine list`; treat inventory as profile data | hardware-software-matrix §6.2 |
+| Wrapper re-layout on a patch bump | wrapper names resolve to wrong addresses | transfer exports/internals only; re-resolve wrappers with `cudatool.py wrappers`; never add a delta to old wrapper addresses | userspace/cuda/docs/07 §3 |
+| Firmware address drift per build | stale Ghidra addresses produce nonsense | re-derive addresses per firmware build; never carry addresses into a new configuration | docs/05-operations §4.1 pitfalls |
+| Host driver assumed to be in lock-step | A/B oracle "updated" and treated as the target | host distro driver is the oracle only; the anchor is guest driver ↔ emulated firmware | docs/05-operations §4.1 |
+| Simulator-only divergence | `compare` delta that physical capture cannot reproduce | record as a fidelity gap; do not change the product from it | docs/06 §6; docs/07 P4.5 |
+| QEMU missing on an aarch64 host | `qemu-system-riscv64 not found` | install `qemu-system-misc` or set `GREENEYE_QEMU` | docs/02 §9; docs/07 P3.5 |
+
+## 6. Roadmap hooks
+
+| Port activity | Roadmap item | What it adds | Gate |
+|---|---|---|---|
+| First-touch map and boot of real firmware | H2 / R1 | device model so ports boot beyond the first unmapped access | portability/accuracy task requires it |
+| Semantic diff of dispatch/handler/control sets | H4.1 | automated portability delta report; per-version symbol corpora | H1+H2 |
+| Capability inference across new devices | H4.2 | access map vs capability model; least-privilege findings | watchpoints (done); TCG-session fold (R4) |
+| Mutation corpus for new input surfaces | H4.3 / R5 | fail-closed rules validated per version | R1/R2 |
+| Differential execution and reimplementation loop | H4.5 | every port lands with A/B sessions and the M1–M6 table | reference-capture pipeline |
+| Adjacent-controller work for GB10-class bring-up | H5 | PMU/SEC2/ACR/SOE models, cross-controller sessions | H2.2 |
+| Multi-target orchestration | R6 / docs/18 | one report across firmware/device targets | v0.1 stable |
+| Version diff and release gating | planned (docs/21) | CI pipeline that gates a port on the evidence bundle | pipeline implemented |
+| Control discovery | planned (docs/19) | systematic control/class inventory across versions | document exists |
+
+Program-level success criterion served here: a new GSP version (or device) can
+be triaged — inventory diff + behaviour diff + metric table — in **hours**,
+with recorded evidence.
diff --git a/debug/risc-v-gsp-rm-vm/docs/23-RM610-READINESS.md b/debug/risc-v-gsp-rm-vm/docs/23-RM610-READINESS.md
new file mode 100644
index 0000000..18d93bd
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/23-RM610-READINESS.md
@@ -0,0 +1,259 @@
+# 23 — RM 610 port readiness and approval gate
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00-OVERVIEW for status.
+> This document defines the gate, not a claim: readiness is **not** met today
+> (2026-10-02). It exists so no one starts the port early.
+
+**The RM 610 port begins only after explicit human approval.** That is a hard
+gate, not a scheduling preference: until a named human approves it in writing,
+nothing in this repository fetches, downloads, extracts or analyses a 610 GSP
+firmware image, and no code or document treats 610 as an anchor or a supported
+target. The gate is repeated in the sign-off checklist (§6) and is the first
+thing the approval message must acknowledge.
+
+## 1. Hard gate
+
+| Rule | Meaning |
+|---|---|
+| No acquisition | No 610 runfile, GSP `.fwimage`, driver package or firmware blob is fetched, extracted, copied or mirrored before approval. |
+| No analysis | No decompilation, symbol export, probe, corpus diff or pairing comparison against a 610 image before approval. |
+| No emulation edits | No version-keyed handler, signature entry or profile edit justified by "610" before approval; changes require current-version evidence. |
+| No physical-GPU work | No unsupervised physical-GPU cycle, mdev claim or driver bind/unbind for 610; the project rule is no unattended physical-GPU cycles (GREEN-EYE-ROADMAP H7). |
+| No support claims | 610 is not entered in the hardware/software matrix or the version matrix until it passes the docs/22 port checklists with evidence. |
+
+Precision notes:
+
+* The pinned `nvidia-open-gpu-kernel-modules` checkout used as build glue
+ (recorded in `SBOM.md`) is **not** a GSP firmware image and is **not** a 610
+ port; it neither authorises acquisition nor counts as analysis.
+* This document, docs/22 and the current-version test runs are the only
+ 610-adjacent work permitted today. Everything else waits for approval.
+* Approval is scoped and revocable: it covers named inputs and named
+ activities, on a named date, by a named approver.
+
+### 1.1 What approval does and does not cover
+
+| Approval covers | Approval does not cover |
+|---|---|
+| Acquiring the named 610 inputs through the named channel | Any other 610 artifact or channel not listed in the approval message |
+| Analysis, probing, diffing and evidence capture on the 610 inputs | Merging an emulation change without the docs/22 A/B evidence |
+| Running the docs/22 port workflow and the pipeline dry-run | Physical-GPU execution without a separate supervised plan |
+| Publishing 610 evidence into the port log | Entering 610 as "Supported" in the matrix before validation |
+| Re-asking for a scope extension | Retroactively blessing work done before approval |
+
+An approval with an open-ended scope is invalid; ask again with the specific
+inputs and activities. A denied or expired approval leaves the repo in the
+state described in §5.
+
+## 2. Readiness criteria (all must be true)
+
+Every row must pass on the **current anchor** (570.144) before approval is even
+requested. "All must be true" is literal: a single open item keeps the gate
+closed.
+
+| # | Criterion | How it is verified | Evidence | Status today |
+|---|---|---|---|---|
+| C1 | All tools implemented and tested on the current driver/firmware | full test suites green (documents/demo/probe/inject/Ghidra/harness) | test logs attached to the request | partial (MCP/CI missing) |
+| C2 | Tests green | `make test` and the harness/rmharness suites pass | transcript with date and commit | green at v0.1 (55 tests) |
+| C3 | Demo, probe, inject, Ghidra work | `./greeneye.sh demo`, `probe`, `inject`, `ghidra` all demonstrated | session dirs + transcript | working per docs/00 |
+| C4 | MCP monitoring works | MCP plugin implemented and exercised (roadmap R3, docs/08; docs/15) | tool list + a monitored run | **not implemented (planned R3)** |
+| C5 | CI pipeline works | release/CI pipeline implemented and exercised (planned docs/21) | pipeline run with artifacts | **not implemented (planned docs/21)** |
+| C6 | Docs complete and accurate | doc review checklist (§3.2) signed | review record | to complete |
+| C7 | Discovery/version-diff/CI exercised on the current version | control discovery (planned docs/19) + `harness/portability_matrix.py` + `compare` + pipeline dry-run on 570.144 | report + session hashes | partial (matrix/compare exist; docs/19 planned) |
+| C8 | Harness produces a full evidence bundle | docs/07 checklist + docs/14 §6 pack produced end to end for a current-version port rehearsal | bundle directory with SHA-256 inventory | to complete |
+| C9 | Human has approved | signed message (§6) | approval ID recorded in the request thread | pending |
+
+C4 and C5 are the long poles. Do not silently downgrade them: if the human
+chooses to waive a criterion, the waiver is written into the approval message
+with its risk and the affected scope.
+
+## 3. What "ready" looks like concretely
+
+### 3.1 Exact commands that must pass
+
+Run from `open-iov/debug/risc-v-gsp-rm-vm/` (docs/02 §8 has the make targets):
+
+```bash
+make demo
+make test
+python3 -m unittest tests.test_harness -v
+python3 rmharness/tests/test_rmharness_plugin.py
+./greeneye.sh info
+./greeneye.sh demo
+./greeneye.sh demo --backend native
+./greeneye.sh probe
+./greeneye.sh inject
+./greeneye.sh symbols export --index <export.tsv> --bias 0 -o symbols-<ver>.json
+./greeneye.sh analyze dumps/<run> --symbols symbols-<ver>.json
+./greeneye.sh compare dumps/<a> dumps/<b> --symbols
+```
+
+Plus, on the current version:
+
+```bash
+python3 harness/portability_matrix.py \
+ --target firmware/uproc_demo/build/uproc_demo.elf \
+ --target firmware/uproc_demo/build/uproc_demo.elf \
+ --out dumps/matrix-selfcheck
+python3 harness/mutation_harness.py --out dumps/mutation-selfcheck \
+ --iterations 12 --seed 7
+python3 harness/mutation_harness.py --out dumps/mutation-replay \
+ --iterations 12 --seed 7
+```
+
+Required results:
+
+* every suite green, with installed QEMU; skips are explained, not ignored;
+* `portability_matrix` self-comparison reports **zero deltas** (`all_zero`);
+* mutation `findings.jsonl` is byte-identical between the two seeded runs;
+* `greeneye compare` on two identical runs is empty;
+* `analyze` renders a coherent session (hook/watch/breakpoint events, UART
+ banner liveness per docs/06 §7).
+
+### 3.2 Doc review checklist
+
+* [ ] 00–18 match the code on the current commit (commands, flags, status
+ tables, test counts);
+* [ ] `docs/22` porting guide is accurate for the current tools; planned items
+ are labelled `planned (docs/xx)`;
+* [ ] `docs/19-CONTROL-DISCOVERY.md` (planned) exists and has been run once on
+ the current version, or C7 is explicitly waived;
+* [ ] `docs/21-CI-RELEASE-PIPELINE.md` (planned) exists and has run once on the
+ current version, or C5 is explicitly waived;
+* [ ] `GREEN-EYE-ROADMAP.md` and `docs/08` status claims match reality;
+* [ ] no local source paths, no invented command names, no unsupported
+ "supported/accurate" claims.
+
+### 3.3 Golden sessions and regression data in place
+
+* [ ] golden demo session(s) checked in or reproducible by a documented seed;
+* [ ] matrix self-comparison zero-delta output archived;
+* [ ] mutation replay corpus archived with its seed and budget;
+* [ ] rmharness host-oracle captures for the current anchor archived
+ (`rmtool engine list --format=json`, `mem stats --format=json`);
+* [ ] a full current-version evidence bundle (docs/07 checklist + docs/14 §6)
+ produced once, as the rehearsal for the 610 run.
+
+## 4. Dry-run plan for the day of approval (no execution today)
+
+Executed only after the approval in §6 exists. Steps 1–2 are acquisition and
+must not happen beforehand.
+
+1. **Confirm the approval.** Record approval ID, approver, date, scope and any
+ waived criterion in the port log.
+2. **Obtain the 610 inputs through the approved channel.** The 610 driver
+ runfile and matching GSP `.fwimage` (or the approved firmware source) are
+ fetched through the channel named in the approval. Record the runfile
+ version, firmware hash (MD5/SHA-256), version string and image family
+ immediately; extract the `.fwimage` per docs/05-operations §4.1 step 1.
+3. **Lay down versioned directories.** `firmware/<SERIES>-GSP/610/`,
+ `userspace/cuda/610/` (copied skeleton), `config/versions/610.yaml`
+ (documentation until the load path reads it). Never overwrite a working
+ version; keep 570.144 untouched.
+4. **Pair and probe.** Build the 610 symbol DB (`analyze_fwimage.py`,
+ `symbols export --bias 0`), probe the 610 image and the 570.144 reference
+ with the same bounded settings, and diff the first-touch maps. Pairing
+ mismatch stops the run here.
+5. **Baseline vs candidate orchestration.** Run
+ `harness/portability_matrix.py` with the 610 and 570.144 targets and
+ identical scenario options; run the mutation harness once per version with
+ the same seed; archive both reports and every session.
+6. **Pipeline dry-run, then real.** Run the release pipeline (planned
+ docs/21-CI-RELEASE-PIPELINE.md) in its dry-run mode first; inspect the
+ planned artifact list without publishing. Only after the dry-run is clean,
+ run the real pipeline on the 610 evidence bundle.
+7. **Expected artifacts.**
+ * approval record and input hashes;
+ * 610 symbol DB + Ghidra export provenance;
+ * probe reports (610 and reference) and the first-touch diff;
+ * `portability-report.json`/`.md`, per-pair compare reports, sessions;
+ * mutation `findings.jsonl` + summary for both versions;
+ * M1–M6 table with reference-capture method and L2/L3 limits;
+ * doc updates per docs/22 §3.5/§4 and the matrix entry (if and only if the
+ support claim is earned).
+8. **Decision points.** Stop and keep the current anchor if: pairing is
+ ambiguous; any unexplained probe delta remains; the engine/RPC inventory
+ diff is not fully explained; M1–M6 regress on the current workload; the
+ evidence bundle is incomplete. Forward only when every delta has a decided
+ response.
+9. **Rollback / keep-old-version rules.** The 570.144 directories,
+ configuration and guest driver stay untouched; rollback = return to the old
+ anchor commit/tag and the old guest driver (docs/05-operations §4.1 step 6).
+ A forward attempt never destroys the working setup; do not delete the old
+ version tree and do not reuse 570.144-derived addresses for 610.
+
+## 5. What must NOT happen before approval
+
+| Prohibited action | Why it is prohibited |
+|---|---|
+| Download/fetch 610 blobs or runfiles | Version discipline and evidence integrity: an unapproved input hash cannot anchor a later claim; acquisition is the act the gate exists to control. |
+| Probe or decompile a 610 image | Analysis before readiness contaminates the evidence chain and tempts ad-hoc emulation edits. |
+| Edit emulation code "for 610" | No current-version evidence justifies it; version-keyed handlers must be driven by a measured inventory diff (docs/05-operations §4.1 step 4). |
+| Touch the physical GPU for 610 | Project hard rule: no unattended physical-GPU cycles; physical validation is a supervised step after approval. |
+| Try the 610 driver in a guest | The guest never contains Open-IOV code and the anchor is fixed; an unpaired driver failure looks like an emulation bug (docs/07 P2.2). |
+| Record 610 in the support/version matrix | Support means tested and documented; an untested entry violates the matrix contract (hardware-software-matrix §1/§6). |
+| Treat the pinned open-module tag as approval | Build glue is not a GSP image and not an authorised port input (SBOM.md). |
+
+Permitted before approval: reading this document and docs/22, running all
+current-version tests, producing and archiving the current evidence bundle,
+and preparing the approval request.
+
+## 6. Sign-off checklist and approval message
+
+### 6.1 Human sign-off (one page)
+
+* [ ] **The RM 610 port begins only after explicit human approval.**
+* [ ] C1–C9 all true, or each waiver is listed with its scope and risk.
+* [ ] Test transcript attached (date, commit, `make test` green).
+* [ ] `portability_matrix` self-comparison zero deltas; mutation replay
+ byte-identical.
+* [ ] Doc review checklist (§3.2) complete.
+* [ ] Current-version evidence bundle complete with SHA-256 inventory.
+* [ ] Golden sessions/regression data in place (§3.3).
+* [ ] Approval message (§6.2) sent and answered with an approval ID.
+* [ ] Named approver accepts the acquisition scope and the permitted channel.
+
+```
+Approver: ______________________ Role: ______________________
+Approval ID: ____________________ Date: ______________________
+Scope limits / waivers: ________________________________________
+Signature: ______________________
+```
+
+### 6.2 Approval request message (exact format)
+
+Send exactly this shape; fill every `<...>` and leave "Waivers" as "none" if
+empty:
+
+```
+RM 610 PORT APPROVAL REQUEST
+
+Anchor today: 570.144 (firmware <sha256>; guest driver <md5>)
+Target: RM 610 (requested input: <runfile version / approved source>)
+Requested scope: acquire the 610 driver + GSP firmware through <approved
+channel>; lay down versioned dirs; analyse, probe, diff, and run the docs/22
+port workflow; no product change before A/B evidence.
+
+Readiness, docs/23 §2:
+ C1 tools tested .......... <pass / waived>
+ C2 tests green ........... <pass / waived>
+ C3 demo/probe/inject/Ghidra <pass / waived>
+ C4 MCP monitoring ........ <pass / waived>
+ C5 CI pipeline ........... <pass / waived>
+ C6 docs complete ......... <pass / waived>
+ C7 discovery/diff/CI ..... <pass / waived>
+ C8 evidence bundle ....... <pass / waived>
+ C9 human approval ........ requested by this message
+
+Waivers: <none / list with reason>
+Evidence bundle: <path>
+Commit: <git rev>; transcripts: <paths>
+Signed: <name/role>, <date>
+
+Decision (human): APPROVED / DENIED
+Approval ID: <id>; Date: <date>; Scope limits: <text>
+```
+
+After approval, the 610 port proceeds only through the docs/22 workflow and
+the docs/05-operations §4.1 version discipline. Until that message exists,
+this repository is a 610 no-fly zone.
diff --git a/debug/risc-v-gsp-rm-vm/docs/24-DEVICE-MAP.md b/debug/risc-v-gsp-rm-vm/docs/24-DEVICE-MAP.md
new file mode 100644
index 0000000..4430bf9
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/24-DEVICE-MAP.md
@@ -0,0 +1,207 @@
+# 24 — Device map: first-touch MMIO and lenient native execution
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status.
+> This is the H2.1 data layer and the first H2.2 building block, not a device
+> model. It carries the same fidelity rules as `06-ACCURACY.md`: L3 remains
+> **absent**; nothing here is a hardware, timing or security claim.
+
+## 1. Purpose and status
+
+`greeneye-device-map/1` records which MMIO addresses a firmware image touched
+first, at which width and direction, with observed values, hit counts and
+provenance. It is JSON data, versioned with the firmware that produced it, and
+consumed by the native simulator's lenient MMIO mode. It exists so the R1
+device work (H2.2) starts from measured addresses instead of guesses, and so
+the real GSP image can be stepped past its first unmapped access to collect
+the *next* ones.
+
+| Piece | Where |
+|---|---|
+| map model, harvest, query, CLI | `greeneye/device_map.py` |
+| lenient / fault MMIO policy | `sim/machine.py` (`Machine(device_map=..., unknown_mmio=...)`) |
+| backend plumbing | `greeneye/sim_backend.py` (`launch_native(..., device_map=..., unknown_mmio=..., learn_mmio=..., bias=...)`) |
+| tests | `tests/test_device_map.py` |
+
+## 2. The model
+
+An entry is one `(address, width, direction)` observation:
+
+| field | meaning |
+|---|---|
+| `address` | the MMIO address as accessed (64-bit; 61-bit GSP addresses fit without allocating memory) |
+| `width` | access size in bytes (1/2/4/8) |
+| `kind` | `"r"` or `"w"` |
+| `values` | observed values (first-seen order, deduplicated); empty for probe-harvested reads because the QEMU log records the fault, not the value |
+| `hits` | number of times observed at this key |
+| `first_epc` | the first observing PC as executed (relocated) |
+| `first_epc_fw` | `first_epc - bias`: the firmware's own virtual address; a PC of 0 (trap-vector fetch) stays 0 |
+| `source` | `probe` (harvested), `manual` (hand-written), `trace` (learned at runtime) |
+| `notes` | fault/exception provenance, e.g. `fault_load reason=rejected` |
+
+The file carries `meta` with the schema version, harvest/learn source, the
+relocation `bias`, `entry`, the firmware path, its `sha256` (when the file is
+readable) and `gsp_version.txt` (sanitised), plus `qemu_fault_count` when
+harvested. Entries are always sorted by `(address, width, kind)` and the file
+is written with `sort_keys=True`, so harvesting the same log twice is
+byte-identical; there are no timestamps.
+
+`DeviceMap.lookup(addr, size, write)` returns a behaviour spec:
+
+* `value:0x...` — a read observed to return this value;
+* `zero` — the address is known (or the direction is unknown) and reads back 0;
+* `unimplemented` — no evidence at this address.
+
+Exact `(address, width, kind)` wins; a different width at the same address is
+still evidence for the register. A write to a read-observed address is
+absorbed; a read of a write-only register reads back zero.
+
+## 3. Harvesting from `greeneye probe`
+
+`greeneye probe` (unchanged) records `blobs/qemu.log` and the parsed
+`qemu_first_fault` in `session.json`. Harvest reads either:
+
+```bash
+# 1) run a bounded probe against the real image (QEMU path, existing command)
+./greeneye.sh probe # writes dumps/<stamp>-probe-<image>/
+
+# 2) turn that session into a device map
+python3 -m greeneye.device_map harvest dumps/<stamp>-probe-<image> \
+ --out dumps/570.144-device-map.json
+
+# 3) inspect / merge maps later, e.g. across firmware versions
+python3 -m greeneye.device_map show dumps/570.144-device-map.json
+python3 -m greeneye.device_map merge a.json b.json --out merged.json
+```
+
+Harvesting the recorded TU104 570.144 probe session
+(`dumps/20261002-091222-probe-gsp-570.144.fwimage`) gives two entries — the
+first-touch MMIO read and the `mtvec=0` fetch loop QEMU falls into afterwards:
+
+```json
+{"schema": "greeneye-device-map/1", "entries": [
+ {"address": 0, "width": 2, "kind": "r", "hits": 367551, "first_epc": 0,
+ "first_epc_fw": 0, "source": "probe", "notes": "fault_fetch reason=rejected"},
+ {"address": 2305843009213696512, "width": 4, "kind": "r", "hits": 1,
+ "first_epc": 2214629396, "first_epc_fw": 67145748, "source": "probe",
+ "notes": "fault_load reason=rejected"}]}
+```
+
+`2305843009213696512 == 0x2000000000000A00`, `67145748 == 0x4009014`; `meta`
+also carries the firmware sha256
+(`2d1bb5c6…`), `firmware_version: 570.144W` and `qemu_fault_count: 367552`.
+Programmatic forms: `harvest_probe_session(session_dir)` and
+`harvest_from_text(log_text, bias=...)` for arbitrary logs; `DeviceMap.save`,
+`DeviceMap.load`, `merge`, `lookup`.
+
+## 4. Lenient MMIO in the native simulator
+
+`sim.machine.Machine` gained three knobs (all default to today's behaviour):
+
+```python
+Machine(memory_mb=256, ram_base=0x80000000,
+ device_map=None, unknown_mmio="fault", learn_mmio=False, bias=0)
+```
+
+* `unknown_mmio="fault"` (default) — **byte-for-byte M1 parity**: an access
+ that is neither RAM nor UART faults exactly as before, and nothing is logged
+ or counted.
+* `device_map=...` — mapped entries are served in *either* mode; a mapped
+ address that is unknown to the map is still `unimplemented`.
+* `unknown_mmio="lenient"` — an unmodelled access returns the mapped
+ behaviour, else zero; it is appended to `machine.mmio_log` as
+ `{pc, addr, size, rw}` and counted in `machine.mmio_unknown`. No logging
+ occurs when lenient mode is off. Debugger `read_mem`/`write_mem` stay
+ strict.
+* `learn_mmio=True` — lenient observations are written back into the map
+ (`source: "trace"`, read value `0`, write value as written); with no map
+ given, one is created lazily. Values recorded with `pc` and `bias`.
+
+Through the backend:
+
+```python
+session = ge.launch(kernel=..., backend="native",
+ device_map="dumps/570.144-device-map.json",
+ unknown_mmio="lenient")
+session.target.mmio_log # [{pc, addr, size, rw}, ...]
+session.target.mmio_unknown # int
+```
+
+`launch_native(..., device_map=..., unknown_mmio=..., learn_mmio=..., bias=...)`
+takes the same options; `device_map` may be a `DeviceMap` or a saved path, and
+`bias` relocates images linked for GPU physical addresses. The lenient path is
+only entered on a miss, so RAM/UART/trace throughput is unchanged; recording
+is a plain list append and the map lookup is a dict hit.
+
+**What lenient proves, honestly:** an unmodelled address can be served a
+mapped/zero value and execution continues deterministically; a bounded run
+lists every unmodelled access and their PCs. It does **not** prove what a real
+device returns, does not model read-back, side effects, interrupts or timing,
+and a zero that keeps the firmware running is a hypothesis to be checked
+against the H2.2 device model (and eventually hardware), not a finding about
+silicon.
+
+## 5. Real-image demonstration (bounded, measured 2026-10-02)
+
+```bash
+python3 -m greeneye.device_map demo --instructions 200000
+```
+
+```text
+image: /home/rmcore/Git/integdev_gpu_drv/open-iov/firmware/57X-GSP/570.144/gsp-570.144.fwimage.bin
+sha256: 2d1bb5c60b208cfd115f015b17dd04a6fece05fd2c0dcba3625d9d48cce3ceea
+elf: entry=0x4009000 vaddr=[0x0,0x5bb9000)
+bias: 0x80000000 relocated entry: 0x84009000
+budget: 200000 instructions
+stopped: reason=budget pc=0x84009070 (fw 0x4009070)
+unknown mmio accesses: 1
+ first unknown: pc=0x84009014 (fw 0x4009014) addr=0x2000000000000a00 size=4 rw=r
+[device-map] qemu probe first fault: fw vaddr 0x4009014 reading 0x2000000000000a00 (load access fault)
+[device-map] verdict: first-touch address reproduced; lenient execution continued for 200000 instructions (199994 past the qemu trap)
+```
+
+Comparison against the QEMU probe (`docs/06 §3`, still current): QEMU executes
+six instructions and traps at firmware vaddr `0x4009014` on the 4-byte read of
+`0x2000000000000A00`, then loops on `mtvec=0` fetch faults. The lenient native
+run reproduces the same first access at the same PC (the `pc` in `mmio_log` is
+the accessing instruction, not the run start), returns 0, and continues
+through the entire 200 000-instruction budget, ending at firmware vaddr
+`0x4009070` with **exactly one** unmodelled access logged. It does not fault.
+
+**61-bit addresses.** `0x2000000000000A00` is a valid 64-bit value in the
+interpreter's address computation; no memory is allocated for it. RAM is
+bounds-checked by `addr - ram_base`, the UART window by range, and everything
+else goes to the device map / lenient handler. (`1 << 61` is well inside the
+RV64 address width; the only care needed is that no map or device code ever
+treats MMIO addresses as host indices.) `--out map.json` saves the learned
+map; `python3 -m greeneye.device_map show map.json` renders it.
+
+## 6. How this feeds H2.2
+
+* The harvested entries are the first test vectors for the `falcon`/`CSB`/
+ `pmu_stub` windows in the docs/16 profile DSL: group entries by page
+ (`address & ~0xFFF`), take `lookup()` behaviour as the initial read-back,
+ and replace `zero` with real semantics one window at a time.
+* `learn_mmio=True` is the bring-up loop: run lenient, inspect the learned
+ map + `mmio_log`, promote hand-verified entries to `source: "manual"`, and
+ re-run strict to see the next unmapped wall. Each promotion should carry a
+ note and the firmware hash, so the map is a reviewable artifact.
+* The map's `bias`, firmware sha256/version and schema make it profile data
+ keyed by `(device, firmware-family, profile-version)`, as required by
+ docs/16 §7.
+* The same maps are the YAML/JSON seed for the QEMU `-device` model, keeping
+ the two backends on one source of truth.
+
+## 7. API and commands
+
+| Call | Purpose |
+|---|---|
+| `harvest_probe_session(dir)` | session dir -> `DeviceMap` |
+| `harvest_from_text(text, bias=0)` | arbitrary QEMU log -> `DeviceMap` |
+| `DeviceMap.lookup(addr, size, write)` | `zero` / `value:0x...` / `unimplemented` |
+| `DeviceMap.record(...)`, `merge(...)`, `save(path)`, `load(path)` | authoring/persistence |
+| `python3 -m greeneye.device_map harvest\|show\|merge\|demo` | CLI |
+| `python3 -m pytest tests/test_device_map.py` | 20 unit/parity/real-image tests |
+
+Testing honesty: the unit tests are synthetic (no QEMU); the real-image test
+is bounded to 200 000 instructions and skips when the fwimage is absent.
+`tests/test_sim_backend.py` remains the M1 parity guard and passes unchanged.
diff --git a/debug/risc-v-gsp-rm-vm/docs/25-VALUE-VALIDATION.md b/debug/risc-v-gsp-rm-vm/docs/25-VALUE-VALIDATION.md
new file mode 100644
index 0000000..d47b8ea
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/25-VALUE-VALIDATION.md
@@ -0,0 +1,337 @@
+# 25 — Value validation: measurable time-to-value evidence
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!** See
+> [00-OVERVIEW](00-OVERVIEW.md) for status. Every number in this document is
+> either **measured** on the bundled demo analog (recorded in
+> `harness/value_demos/artifacts/`) or explicitly labelled a **projection**
+> for the real case. No demo claims a real-GSP result the simulator cannot
+> reach; device-model-blocked scenarios say so in their result.
+
+## 1. Why this document exists
+
+green-eye is only worth its maintenance if it turns real firmware questions
+into answers faster than static analysis and supervised hardware cycles. The
+project handoffs record those questions and their costs in wall-clock and
+cycles: the 0x59 RM-init chase (PART 91, 2026-10-01), the 20 GR counters
+(PARTs 46–64, 2026-09-24…29), the GPFIFO GET fault (PART 91 ADDENDUM 9), the
+P6 service wiring (PART 92 ADDENDA 5–11, 2026-10-02), and the version pairing
+question (570.144 vs 570.211). This document maps each of them to a runnable
+demonstration under [`harness/value_demos/`](../harness/value_demos/), states
+the baseline cost from the handoffs, and measures the green-eye path.
+
+## 2. Methodology
+
+### 2.1 What is measured, what is projected
+
+| Class | Meaning | Where it appears |
+|---|---|---|
+| **Measured (analog)** | Wall-clock of the green-eye path on the bundled `uproc_demo` firmware, native RV64 interpreter or QEMU; answers, counts, session bytes, trace slowdown | `result.json` per demo, `artifacts/results.json` |
+| **Recorded (real baseline)** | Cost actually paid on the physical path, as recorded in the handoffs (dates, cycles, PARTs, wall spans) | `real_case.static_baseline` in each `result.json`, §5 below |
+| **Projected** | What the same tool path would save on the real firmware once H2.2 executes it; labelled `projected` | `real_case.projection`; never presented as a result |
+| **Blocked** | Real firmware access requires the H2.2 QEMU device model; `blocked_on_device_model: true` | every real `.fwimage` scenario here |
+
+Fidelity is exactly docs/06: L0 instruction semantics and L1 memory behaviour
+are exercised; **L2 time/concurrency is not modelled** and **L3 device
+environment is absent**. A green-eye observation is evidence about the
+firmware, and anything that would change `open-iov-vfio` is escalated to a
+physical capture before the product changes (docs/06 §6, docs/14 §7).
+
+### 2.2 Run contract
+
+* Each demo is a stdlib-only script run from the tool root; it prints the
+ question, commands, answer, evidence and elapsed time, then one
+ `RESULT_JSON: {...}` line.
+* `run_all.py` executes each demo in a subprocess with a hard timeout,
+ aggregates `artifacts/results.json`, and enforces the suite budget
+ (per-demo ≤ 45 s, suite ≤ 180 s).
+* Demos are deterministic: native sessions use virtual time (instruction
+ count), stimuli are fixed, and the version-diff patch site is verified
+ unique before use. Artifacts contain no host paths beyond the repo.
+
+### 2.3 Scenario selection and “why not edge cases”
+
+Every class below recurs across multiple handoff PARTs and sits on the
+critical path to `vectorAdd`. The mapping is direct:
+
+| Demo | Recurs in | Why it is on the vectorAdd path |
+|---|---|---|
+| s01 reply consumer | PART 91 cycles 2–13; status-queue decode cycles 22–25 | RM init must complete (`0x1001`, `rpc_result=0`) before any RPC; a generic failure hides the first error |
+| s02 counter publication | PARTs 46–64 | `cuCtxCreate` completes on per-channel counters; 4/20 blocked the client |
+| s03 unknown commands | every new image; docs/19 | control inventory (M1) is the input surface the emulation must handle |
+| s04 run-until | PART 91 ADDENDUM 9 (cycles 18–25) | each ladder rung (VAS, PDB, channel, token, GPFIFO, semaphore) is a reachability question |
+| s05 snapshot diff | PART 91 ADDENDUM 6/7 (identity, static config) | static-config fields feed guest wiring; a zeroed field kept the device-id read at zero |
+| s06 inject service | PART 92 ADDENDA 5–11 (P6a–P6c) | guest-originated work executing on the engine is the shape of the load |
+| s07 version diff | PART 91 cycles 2–3; every GSP release | the emulation must track firmware changes without regressing |
+| s08 mutation triage | docs/18 R5; docs/19 §3.4; PARTs 3–9 | fail-closed rules come from error paths, not happy paths |
+| s09 Ghidra address | PART 91 ADDENDUM 9 cycle 24 | the GPFIFO GET fix was accepted only after the runtime fault PC existed |
+| s10 evidence pack | docs/14 §6; every milestone | a finding without sessions/hashes/M1–M6 is not actionable |
+
+None of these is a bizarre edge case: they are the same observe → decode →
+change loop the project runs on every firmware version, and each one is
+cited more than once in the handoffs.
+
+## 3. Scenario → tool → baseline → measured
+
+Measured on the reference host (CPython 3.12); numbers regenerate with
+`python3 harness/value_demos/run_all.py`. Times below are from the
+`run_all` aggregate (~18.4 s total).
+
+| # | Real scenario (source, date) | Green-eye tool | Static/Ghidra-only baseline (recorded) | Analog (measured) | Real-case projection |
+|---|---|---|---|---|---|
+| s01 | 0x59 RM-init chase; fn72/fn73 (PART 91 + ADDENDA 2–7, 2026-10-01) | hook dispatcher, watch `result`, decode reply | cycles 2–13, ~11:40→16:52 ≈ 5 h 10 m; 10+ supervised boots (~5–6 min each); reply was generic `0x59` | **1.14 s**; 3/3 commands consumed and decoded; FAULT traps before the reply store | one hook + one watchpoint per command once H2.2 boots the image (blocked) |
+| s02 | 20 GR per-channel counters (PARTs 46–64, 2026-09-24…29) | watch publication field, per-slot readback | ~5 days; PART 49 “only 4 of 20 counters ever moved”; ~2–3 s pin vs ~2 s client deadline | **1.34 s**; 20/20 slots published, 28 stores, store PCs captured | per-channel page watchpoints on real counter pages (blocked) |
+| s03 | unknown control IDs / injected slots (docs/19, 2026-10-02) | sweep + dispatcher/handler hooks + classify | 189 real probes in ~36 s; static-only requires the indirect table per image | **5.96 s** for 24 IDs: 6 known, 8 injected-slot (aliases shown), 10 default | RM control/method sweep once H1.3/H2.2 exposes the table (blocked) |
+| s04 | RM-client ladder reachability (PART 91 ADDENDUM 9, 2026-10-01) | `run_until`, trigger, direct `call` | cycles 18–25 = 19:12→20:53 ≈ 1 h 41 m, 8 supervised boots | **0.24 s**; natural hit / timeout / trigger / direct call | every ladder rung becomes a probe spec (blocked) |
+| s05 | fn72 identity + 1656-B static config (PART 91 ADDENDUM 6/7, 2026-10-01) | before/after snapshot + schema diff | malformed fields suspected statically; acceptance required a live boot per iteration (10 NOCAT cycles) | **0.50 s**; 6 named fields changed, 7 byte runs fully attributed | same diff over real RM control/reply pages (blocked) |
+| s06 | P6 service wiring / guest CE copies (PART 92 ADDENDA 5–11, 2026-10-02) | service injection SDK, both invocation routes | kernel rebuild + supervised cycle (~5–8 min) per rung; P6a→P6c spanned ~02:20–08:30 | **0.63 s**; blob loaded/registered; remote and mailbox results agree | RM service registry (M5) once H2.2 exists (blocked) |
+| s07 | 570.144 vs 570.211 pairing (PART 91 cycles 2–3, 2026-10-01) | same stimulus on two builds, `compare_sessions` | one full boot (~5–6 min) per version plus manual log comparison | **1.14 s**; only the patched reply field moved; decision-table row = “prioritise” | real versions triaged on re-hosted firmware; symbols/sessions are the artifact (blocked for the raw image) |
+| s08 | mutation / fail-closed corpus (R5; docs/18 §2; docs/19 §3.4) | seeded one-field mutations, signal classification | hand-derived asserts/NOCAT across PARTs 3–9; a 64-iteration QEMU run is the tracked harness baseline | **1.82 s**; 7 mutations → {ok:4, fault:2, no-progress:1}, all expectations matched | real controls land in the same harness (H1.3 surface, blocked) |
+| s09 | GPFIFO GET fault `0x40000` vs `0x101000` (PART 91 ADDENDUM 9 cycle 24) | Ghidra TSV + bias + raw-address hook + `export_hits` | static hypothesis unproven; the decoded fault ended 8 supervised cycles (~1 h 41 m) | **0.72 s**; biased address resolves, hook fires at the exact PC, TSV annotated | names.tsv + bias loop once the image executes (blocked) |
+| s10 | milestone evidence pack (docs/14 §6; cycles 11–13, 2026-10-01) | one recorded session → analyze/compare/hits/manifest/M1–M6 | hashes, sessions, logs and compare gathered by hand across boots | **4.92 s**; 12,276-byte session, A/B deterministic, all artifacts + overhead numbers | same pack against real sessions (blocked) |
+
+## 4. Exact commands (per demo)
+
+All from `open-iov/debug/risc-v-gsp-rm-vm`. `VALUE_DEMO_BACKEND` selects the
+backend (`native` default, `qemu` where noted).
+
+```bash
+# s01 — reply consumer (native; QEMU also supported)
+python3 harness/value_demos/s01_reply_consumer.py
+# hook("rpc_dispatch"); watch(gsp_ctrl+0x40, 8, 'w')
+# mailbox cmd=1 arg0=0x1234 -> 0xacdb; cmd=7 -> ~7; cmd=6 -> trap, no result store
+
+# s02 — counter publication
+python3 harness/value_demos/s02_counter_publication.py
+# watch(gsp_ctrl+0x40, 8, 'w'); 20 x (mailbox cmd=5, run) -> 20/20 published
+
+# s03 — unknown command discovery
+python3 harness/value_demos/s03_unknown_command.py
+# fresh session per ID 1..24; hook dispatcher + 6 handlers; classify
+# (findings.jsonl, control-discovery.md)
+
+# s04 — run-until / reachability
+python3 harness/value_demos/s04_run_until_function.py
+# run_until("rpc_sum"); run_until("rpc_fault", timeout=0.2) -> RspTimeout;
+# mailbox cmd=6 + run_until; call("rpc_ping", (0x41,))
+
+# s05 — snapshot diff
+python3 harness/value_demos/s05_snapshot_diff.py
+# snapshot before/after ECHO; Memory.diff; decode with gsp_ctrl.schema.json
+
+# s06 — service injection (needs `make sdk`)
+python3 harness/value_demos/s06_inject_service.py
+# load_service(...slot=6); invoke_remote; invoke_via_firmware
+
+# s07 — version diff
+python3 harness/value_demos/s07_version_diff.py
+# byte-patch the PING key (0xbeef -> 0xbef0) into version B; identical
+# stimulus; decoded field diff + compare_sessions
+
+# s08 — mutation triage
+python3 harness/value_demos/s08_mutation_triage.py
+# 7 seeded one-field mutations; classify by stop signal + rpc_count
+
+# s09 — Ghidra address join (QEMU by default)
+python3 harness/value_demos/s09_grabbing_a_ghidra_address.py
+# names.tsv (program = runtime - 0x1000); load_names_tsv(bias=0x1000);
+# hook(ghidra + bias); export_hits -> greeneye_hits.tsv
+
+# s10 — full evidence pack (QEMU by default)
+python3 harness/value_demos/s10_full_session_evidence.py
+# two identical recorded sessions; analyze + compare + hits + manifest + M1–M6
+# + startup/session/trace overhead
+
+# all of them
+python3 harness/value_demos/run_all.py
+python3 -m unittest tests.test_value_demos -v
+```
+
+## 5. Per-scenario detail
+
+### s01 — reply consumer (0x59 first-error visibility)
+
+The physical path sent `GSP_SET_SYSTEM_INFO` (fn72) and `SET_REGISTRY`
+(fn73), and the only visible reply was `GSP_INIT_DONE rpc_result=0x59`
+(`NV_ERR_OPERATING_SYSTEM`), wrapped generically by `RmStartup`; the RM log
+ring was not mapped until the LOGRM id fix (`0x4c4f474d` “LOGM” vs
+`0x4c4f47524d` “LOGRM”), so the failing collector step was invisible
+(PART 91 ADDENDA 2–6). Ten-plus supervised boots later, the identity fix
+(raw config-space dwords instead of unpacked fields) made init-done succeed
+in 950 ms. The analog consumes each posted command, watches the result
+field, and decodes the exact reply — including the case where the fault
+handler traps before any reply store. **Success:** 3/3 dispatches, replies
+match the ABI, FAULT has no result store. **Failure:** a command is not
+consumed or a reply disagrees.
+
+### s02 — counter publication (20 slots)
+
+`cuCtxCreate` waits on 20 GR per-channel counters; the host saw the client’s
+releases but could not translate the counter page, and for days only 4 of 20
+client-visible slots moved (PART 49, 2026-09-29). The page VA
+(`0x20400f000`, slots at `+0xec0`, 16-byte stride) and the different GPA per
+boot were only proven by a raw live page dump (PARTs 51, 64). The analog
+publishes 20 slots and attributes each store to a PC. **Success:** 20/20
+published with one `rpc_count` increment per slot. **Failure:** any slot
+missing or count delta disagreeing.
+
+### s03 — unknown command discovery
+
+New firmware can silently add or re-point dispatch entries; static reading of
+an indirect table is per-image work. The 2026-10-02 sweep (189 probes, ~36 s)
+found IDs 16–23 falling through to the injected-service handler table
+(16–21 alias built-ins, 22–23 NULL returning `~id`) and 7–15/24–63 on the
+default path (docs/19 §3.4). The analog sweeps 24 IDs with fresh sessions
+and the same hook shape. **Success:** expected classes per ID. **Failure:**
+a misclassification or an unreplayable probe.
+
+### s04 — run-until reachability
+
+The RM-client ladder asked per rung whether symbolized code executed under
+the current stimulus; cycles 18–25 cost ~1 h 41 m of supervised boots
+(PART 91 ADDENDUM 9). The analog shows three answers in one session: reached
+on the natural path, timed out (not reached under this stimulus — docs/14 §5
+explicitly says this is not unreachability), reached via a trigger, and a
+direct Frida-like call. **Success:** all four behaviors agree with the ABI.
+**Failure:** a route reports reachable without a hit, or a direct call
+disagrees.
+
+### s05 — snapshot diff
+
+The fn72 payload carried unpacked identity fields where the reference sends
+raw config-space dwords; the server read the device id back as zero, and the
+fix was confirmed by rejecting hypotheses field-by-field (PART 91 ADDENDUM 6,
+cycle 12), then by decoding the full 1656-byte static-config reply (cycle
+13). The analog diffs two snapshots and attributes every changed byte run to
+a schema field. **Success:** `rpc_count`/`last_rpc`/`result`/`buf` change,
+`result` equals the ECHO length, no unattributed run. **Failure:** an
+unattributed changed region.
+
+### s06 — service injection
+
+P6 wired guest-originated copies through the real engine; every rung was a
+kernel rebuild plus a supervised cycle with fail-safe release discipline
+(PART 92 ADDENDA 5–11, 2026-10-02). The analog loads `injected_demo.elf`,
+registers it in the dispatch table, and invokes it directly and through the
+mailbox. **Success:** both routes agree with the SDK ABI. **Failure:** a
+route disagrees, the slot is not registered, or the blob cannot load
+(skipped with a reason when `make sdk` has not run).
+
+### s07 — version diff
+
+Cycles 2 (570.211) and 3 (570.144) both failed init with `0x59`, disproving
+the image-pairing hypothesis at the cost of one full boot each, and the
+project’s H4.1 decision table exists to separate “the code changed” from
+“the behaviour moved” (docs/14 §4.1). The analog patches the PING reply
+constant into a labelled version B and replays an identical stimulus; only
+the patched field moves. **Success:** exactly the patched reply moves.
+**Failure:** unrelated movement or an unreplayable version.
+
+### s08 — mutation triage
+
+The fail-closed rules come from error paths; the tracked harness is bounded
+seeded mutation with outcome classes (docs/18 §2), and the discovery sweep
+found the sentinel/boundary pointer faults (docs/19 §3.4). The analog runs 7
+one-field mutations in fresh sessions with `max_stops=1` (a faulted target
+re-faults at the same PC, so the run stops at the first signal) and
+classifies ok/trap/fault/no-progress. **Success:** every mutation matches
+its case table. **Failure:** a contradicting class.
+
+### s09 — grabbing a Ghidra address
+
+The GPFIFO GET hypothesis (`0x101000`) was only settled when the decoded
+status queue showed the fetch at `0x40000`; fix `6f9cbfa` followed
+(PART 91 ADDENDUM 9, cycle 24). The analog exports a Ghidra-style
+`names.tsv` with a deliberate base offset, loads it with the matching bias,
+hooks the raw static address, triggers the path, and writes
+`greeneye_hits.tsv` back for Ghidra. **Success:** biased address equals the
+real function, hook records the hit, TSV annotates it. **Failure:** a hit at
+a different address or an empty TSV.
+
+### s10 — full session evidence
+
+The evidence pack contract (docs/14 §6) was satisfied by hand for every
+milestone (cycle evidence dirs, P6 dirs). The analog produces it in one run:
+two deterministic sessions, analyze, compare, hits TSV, firmware/artifact
+hashes, the M1–M6 skeleton (M2/M5 marked not measured without a physical
+reference), and the measured overhead. **Success:** ABI reply, A/B
+agreement, all artifacts, empty unexplained-delta list. **Failure:**
+nondeterminism or a missing artifact.
+
+## 6. Overhead analysis (measured, with numbers)
+
+| Overhead | Measured on the analog | Real-path comparison |
+|---|---|---|
+| Session startup | native ≈ 0.001 s; QEMU ≈ 0.13 s per launch | one supervised hardware cycle ≈ 5–6 min (PART 91/92) |
+| Session size | 12,276 B per recorded session (s10), including 2 snapshots and 16 event types | per-cycle evidence dirs hold multi-MB logs, rings and decodes |
+| Trace slowdown | 3.1× run-only on the demo workload; 13.2× end-to-end including writing `blobs/trace.jsonl` (~200 k exec lines) | docs/16: ~2.4× exec trace on the demo workload (1.53 → 0.54 MIPS); guard ~7% |
+| CI cost | `tests/test_value_demos.py`: 5 tests in ≈ 4.5 s; `run_all.py`: 10 demos in ≈ 18.4 s (budget 180 s) | a single physical cycle is 300–400× the entire demo suite |
+| Host footprint | stdlib only; artifacts under `harness/value_demos/artifacts/`; no hardware, no guest, no kernel module | physical cycles hold D0/D3cold locks, mdev and a QEMU guest |
+
+**Why the overhead is dominated by the time saved.** The expensive baselines
+are per-question hardware cycles and multi-hour manual campaigns (s01 ≈ 5 h,
+s02 ≈ 5 days, s04/s09 ≈ 1 h 41 m each, s06 ≈ 6 h). The tool path for the same
+question class costs 0.2–6.0 s on the analog plus ≤0.13 s of launch — under
+7 s end to end for every demo, or ~0.02 % of one 5–6-minute cycle even
+before the campaign time. Trace logging at 3–13× of a sub-second run is
+still seconds; a 12 KB session replaces a multi-MB manual capture. The only
+place the tools add real cost is the once-per-question scripting in
+`harness/value_demos/` and the 4.5 s test, which run in CI and are reused by
+every later version.
+
+## 7. OODA-loop compression
+
+| Stage | Before (real path, as recorded) | After (green-eye analog, measured) |
+|---|---|---|
+| **Observe** | one supervised boot + capture per hypothesis: 5–6 min, fail-safe discipline, one device-write experiment per cycle | launch 0.001–0.13 s, then hooks/watchpoints and bounded runs: 0.2–5.8 s per question |
+| **Orient** | manual LOGINIT/LOGRM/NOCAT ring decode, static reports (REPORT3–10), hours per campaign; the ring was invisible until cycle 11 | `analyze`/`compare`/`Memory.diff` with named symbols: seconds, e.g. s05 attributes every changed byte to a schema field |
+| **Decide** | one candidate fix per supervised cycle, accepted/rejected by another boot | version diff (s07), mutation matrix (s08) and per-slot publication (s02) test the whole candidate set in one deterministic run |
+| **Act** | rebuild + supervised boot; escalate every conclusion to a physical capture | re-run the session (seconds); product change still gated on physical confirmation (docs/06 §6, docs/14 §7) |
+
+The compression is not “the simulator replaces the hardware”; it is the loop
+around each physical experiment. green-eye reduces the number of physical
+cycles needed to reach a confident hypothesis and produces the evidence pack
+that makes the eventual physical confirmation cheap (s10).
+
+## 8. Success/failure criteria summary
+
+| Demo | Pass | Fail |
+|---|---|---|
+| s01 | 3/3 dispatches; PING/unknown replies match; FAULT traps before the store | command not consumed or reply disagrees |
+| s02 | 20/20 slots published, `rpc_count` +20 | any slot missing / count mismatch |
+| s03 | expected class per ID (6 known, 8 injected, 10 default) | misclassification or unreplayable probe |
+| s04 | natural hit, timeout, trigger hit, direct call value | false reachability or wrong call value |
+| s05 | required fields change, result = ECHO length, no unattributed run | unattributed changed region |
+| s06 | remote and mailbox invocation agree with the SDK ABI | route disagreement or unloadable blob (skipped if SDK absent) |
+| s07 | only the patched field moves | unrelated movement / unreplayable version |
+| s08 | every mutation matches its expected class | contradicting class |
+| s09 | biased address resolves; hook fires; TSV annotated | wrong address / empty TSV |
+| s10 | ABI reply; A/B agreement; artifacts complete; no unexplained deltas | nondeterminism / missing artifact |
+
+## 9. Reproducing and extending
+
+```bash
+cd open-iov/debug/risc-v-gsp-rm-vm
+make demo && make sdk
+python3 harness/value_demos/run_all.py # writes artifacts/results.json
+python3 -m unittest tests.test_value_demos -v # fast gate
+```
+
+`artifacts/results.json` is the machine-readable source for the table in §3:
+per-demo `elapsed_s`, `success`, `real_case`, answers, and artifact paths.
+When the H2.2 device model lands, each demo's `projected` note becomes the
+runbook for replacing its analog with the real image; the result format and
+evidence contract do not change.
+
+## 10. Limits and fidelity statement
+
+* The analog firmware is not GSP-RM: no RM objects, status queue, object
+ model or GPU device model. Demos reproduce the *shape* of each scenario
+ (hook the dispatcher, watch the field, decode the reply, diff versions,
+ mutate, inject) and are labelled as such.
+* L2 timing/concurrency is not modelled; no demo makes a timing claim.
+* L3 device environment is absent; every real-firmware result is
+ `blocked_on_device_model`.
+* Absence of a hit is a fact about that run at that fidelity, not proof of
+ dead code (docs/14 §2).
+* Full suite status and per-demo times are regenerated by
+ `run_all.py`; do not quote a stale number without re-running it.
diff --git a/debug/risc-v-gsp-rm-vm/docs/26-ARCHITECTURE.md b/debug/risc-v-gsp-rm-vm/docs/26-ARCHITECTURE.md
new file mode 100644
index 0000000..326533b
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/26-ARCHITECTURE.md
@@ -0,0 +1,449 @@
+# 26 — green-eye architecture: full picture, workflow and API surfaces
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!**
+> The architecture map for the green-eye suite: how the pieces fit, which
+> surface to call, and which document specifies each part. Every claim here is
+> implemented in the tree or explicitly marked as a gated roadmap design.
+> Development-only: never shipped, never a driver dependency, never in a guest.
+
+## 1. What green-eye is (and is not)
+
+green-eye runs GSP/uproc firmware in a controllable machine, observes it with
+Frida-like primitives, records a durable session, changes the machine's inputs
+(including injected code), and compares runs as evidence for `open-iov-vfio`
+emulation changes.
+
+| It is | It is not |
+|---|---|
+| A firmware runtime microscope (execution + memory, L0/L1) | A time, interrupt or device model (L2/L3 absent — [06](06-ACCURACY.md)) |
+| A scriptable `Session` API with two backends | A GDB wrapper; there is no GDB dependency |
+| A recorded-evidence system with a versioned format | A product component or a guest agent |
+| A static/dynamic join with Ghidra | A decompiler or a CUDA-corpus replacement |
+| An automation layer (harnesses, MCP, CI) | A fuzzing service against hardware; simulator/re-hosted code only |
+
+## 2. The suite as a whole
+
+```text
+ front doors control plane evidence
+ CLI/REPL/scripts ─┐
+ MCP (stdio) ──────┼─► GreenEyeAPI ─► Session ─┬─► QEMU-RSP backend ──┐
+ Ghidra bridge ────┤ (plugins) Memory │ qemu-system-riscv64│
+ harness/ ─────────┤ SymbolDB └─► native backend ────┼─► session dir
+ rmharness ────────┘ Recorder sim/machine.py │ events.jsonl
+ └─► analyze / compare /
+ export_hits / bundles
+```
+
+```mermaid
+flowchart LR
+ CLI["CLI/REPL/scripts"] --> CORE
+ MCP["MCP server"] --> CORE
+ GH["Ghidra bridges"] --> CORE
+ HAR["harness/*.py"] --> CORE
+ RMH["rmharness (read-only)"] --> CORE
+ CORE["GreenEyeAPI + Session<br/>hooks, watchpoints, call, run_until<br/>Memory, SymbolDB, Recorder"]
+ CORE --> QB["QEMU-RSP backend"] --> SESS
+ CORE --> NB["native backend (M1/M1.5)"] --> SESS
+ SESS["session dir, greeneye-session/1"] --> REP["analyze / compare / export_hits / MCP / bundles"]
+ RMH --> SESS
+```
+
+Status: transport, core, recording, both backends (native M1 trace included),
+injection ABI v1, both Ghidra bridges, traces, device map, harnesses, MCP (R3)
+and `rmharness` are implemented and exercised. The `Backend` protocol
+refactor (16 §5), the H2.2 QEMU device model, RM-core injection (H1.3) and the
+H3 M2–M6 milestones are design/roadmap only ([16](16-CUSTOM-SIMULATOR.md)).
+
+## 3. Session core: hooks, watchpoints and dispatch
+
+`Session` ([greeneye/session.py](../greeneye/session.py)) owns all stop-driven
+state and is backend-agnostic: every stop reads registers, resolves the PC,
+records an event, invokes matching hooks/watchpoints, then resumes.
+
+```mermaid
+sequenceDiagram
+ S->>FW: set breakpoint / watchpoint
+ loop run(duration, max_stops)
+ FW-->>S: stop reply (T/S/W)
+ S->>S: resolve PC via SymbolDB
+ S->>R: bp_hit / watch_hit / stop / step
+ alt symbol entry
+ S->>H: on_enter(hit) with a0..a7
+ S->>S: arm return breakpoint from ra (LIFO)
+ S->>FW: continue
+ else return breakpoint
+ S->>H: on_leave(hit), a0 as retval
+ else watchpoint (Z2/Z3/Z4)
+ S->>FW: clear watch, step over store, re-arm
+ S->>H: callback + store PC
+ end
+ S->>R: on_event(kind, hit)
+ end
+ S->>R: session_end on close
+```
+
+| Primitive | Semantics / limits |
+|---|---|
+| `breakpoint(addr, kind=sw\|hw, temporary=)` | sw is stepped over transparently; hw uses the backend's `Z1` |
+| `hook(target, on_enter, on_leave)` | leave is a return breakpoint on `ra`; tail calls/interrupt returns are not tracked |
+| `watch(target, size, kind=w\|r\|a, callback)` | QEMU stops *at* the access; Session clears/steps/re-arms; store PC is recorded |
+| `run(duration, max_stops)` | services stops until a budget or exit; the deadline path interrupts once |
+| `run_until(target, timeout)` | temporary breakpoint; raises `RspTimeout` when not reached |
+| `call(target, args, timeout)` | Frida-like remote call; restores `RESTORE_REGS` around it |
+| `step(n)`, `note(text)`, `on_event(kind, fn)` | single-step with dispatch; annotate and observe the stream |
+
+Instructions between stops are invisible unless the TCG plugin
+(`qemu/greeneye_trace.c`) or the native trace (`sim/trace.py`) is on
+(01 §11, 16 §1.1a).
+
+## 4. Backend abstraction: QEMU-RSP and native
+
+`Session` assumes a small RSP-shaped primitive set; `run_until` and `call`
+live above it and work on every backend. M1 implements the same method
+surface (`greeneye/sim_backend.py`) without the planned `Backend` protocol
+refactor (16 §5); the table is the M2+ target.
+
+```mermaid
+flowchart LR
+ S["Session (backend-independent)"]
+ S -->|"read/write reg+mem, step, continue, Z0-Z4"| RC
+ S -->|"same surface"| ST
+ subgraph QEMU["QEMU-RSP backend (default)"]
+ QT["QemuTarget<br/>qemu-system-riscv64 virt"] --- RC["RspClient gdbstub packets"]
+ end
+ subgraph NAT["Native backend (--backend native)"]
+ ST["SimTarget / SimClient"] --- MC["sim.machine.Machine<br/>RAM + UART + decode loop"]
+ MC --- TR["sim.trace.Trace exec/access"]
+ MC --- DM["DeviceMap lenient MMIO"]
+ end
+```
+
+| Session need | QEMU-RSP | Native |
+|---|---|---|
+| registers/memory | `g`/`p`/`P`, `m`/`M`/`X` chunked to `PacketSize` | direct register file and memory map |
+| breakpoints/watchpoints | `Z0`–`Z4` | dispatch-loop tables; same stop semantics |
+| snapshots | region copies over RSP | direct map copy; machine moments are M2+ |
+| virtual time | host wall clock | deterministic instruction count (`SIM_IPS`) |
+| full-access trace | TCG plugin (`-plugin`) | in-process `sim/trace.py`, `--trace` |
+| device environment | QEMU `virt` only (no TU104) | device map + lenient MMIO; real devices H2.2 |
+| injection | debugger writes + `Session.call` | same SDK ABI; RM interception is M5 |
+
+`--backend qemu|native` is accepted by `demo`, `run`, `attach`, `record`,
+`inject` and `ghidra`; `attach --port` and QEMU `--trace` are QEMU-only;
+`--trace` is native-only (16 §1.2).
+
+## 5. Recording and the evidence format
+
+A session directory is the durable artifact (`greeneye-session/1`,
+[greeneye/record.py](../greeneye/record.py)). The event stream is the map; the
+snapshot and blob files are the dump.
+
+```text
+dumps/<stamp>-<name>/
+ session.json # format, name, created, counts, snapshot inventory, meta
+ events.jsonl # append-only JSON lines: {"t": monotonic, "type": ...}
+ snapshots/*.bin.z # zlib-compressed region captures (sha256 recorded)
+ blobs/* # QEMU logs, trace.jsonl, rmharness stdout, plugin dumps
+ summary.md # rendered analysis
+```
+
+```mermaid
+flowchart LR
+ E["Session stops, memory ops,<br/>probe/plugin/injection events"] --> R["Recorder.event()"]
+ R --> J["events.jsonl (append-only)"]
+ M["Memory.snapshot()"] --> Z["snapshots/*.bin.z + sha256"]
+ B["Recorder.blob()"] --> BD["blobs/*: trace.jsonl, qemu.log"]
+ X["rmharness external runs"] --> J
+ J --> A["analyze: summary.md"]
+ J --> C["compare: A/B delta report"]
+ J --> G["export_hits: greeneye_hits.tsv"]
+ J --> S10["value_demos s10: manifest + M1-M6 skeleton"]
+```
+
+Event classes: core stop/memory events (`session_start`, `image_loaded`,
+`hook`, `watch`, `bp_hit`, `leave`, `watch_hit`, `stop`, `step`, `mem_read`,
+`mem_write`, `snapshot*`, `probe*`, `plugin*`, `note`, `exit`, `session_end` —
+01 §6); injection (`service_injected`, `call_enter`, `call_return` — 12 §6);
+traces (`exec`/`access`); M2/M3 native bus (`device`/`irq`/`schedule` — 16
+§10); `rmharness` (`rm_harness_run`, `t=0.0` with epoch `start`/`end` — 17 §5).
+
+Format invariants: append-only (partial sessions stay readable); additive
+versioning with migration notes (V2); `compare` treats `t`, host paths and
+backend identity as non-semantic; a claim without a session is not evidence
+(06 §7, 25 §1).
+
+## 6. Injection SDK lifecycle
+
+The SDK (`sdk/greeneye_service.h`, `sdk/injected_demo.c`) builds
+position-independent ELF64 `EM_RISCV` blobs; `greeneye/inject.py` loads,
+registers and invokes them. ABI v1: `ge_u64 (*)(ge_u64 cmd, ge_u64 a0,
+ge_u64 a1)`, magic `0x47455356`, entry `ge_demo_service_dispatch`.
+
+```mermaid
+sequenceDiagram
+ Dev->>Inj: load_service(elf, slot=6, load_addr)
+ Inj->>FW: write PT_LOAD segments into RAM
+ Inj->>FW: patch gsp_ctrl.handlers[slot] entry
+ Inj->>Rec: service_injected + mem_write events
+ Dev->>Inj: invoke_remote(cmd, a0, a1)
+ Inj->>FW: Session.call(entry, args)
+ Rec-->>Rec: call_enter / call_return
+ Dev->>FW: mailbox = 16 + slot (arg0=cmd, arg1, arg2)
+ FW->>FW: dispatch handlers[slot]
+ FW-->>Rec: hook/watch events + result field
+```
+
+Demo registration: mailbox commands 16..23 select slots 0..7; slot 6 is the
+SDK default; control-block offsets (`mailbox +0x20`, `handlers +0x150`) are
+demo-specific and must come from `greeneye/inject.py` constants (12 §4).
+Cleanup is not automatic in v1; record the injection so runs start from a
+known state. Roadmap: RM-core shim, service registry and `log`/`stub`/
+`forward` interception at M5/H1.3 (12 §8, 16 §11).
+
+## 7. Ghidra bridges (two directions)
+
+The static/dynamic join uses three address spaces — file vaddr, Ghidra program
+addr, runtime addr — with a bias per source (01 §5, 05 §1).
+
+```mermaid
+sequenceDiagram
+ G->>B: {"id":n,"cmd":"run_until","args":{"target":"0x..."}}
+ B->>S: run_until(addr) (temporary breakpoint)
+ S->>T: continue; service intervening stops
+ T-->>S: stop at addr
+ S-->>B: {addr, symbol, kind}
+ B-->>G: JSON response
+ G->>B: read_mem / read_reg / call / snapshot / hits / export_hits
+ B->>R: hits and notes recorded in the session
+ R-->>G: greeneye_hits.tsv (via MarkGreenEyeHits.java)
+```
+
+| Tool | Purpose | Doc |
+|---|---|---|
+| `symbols export` + `SymbolDB` loaders | Ghidra TSV/JSON → runtime symbol DB | [05](05-GHIDRA-BRIDGE.md) |
+| `greeneye.ghidra.export_hits` | session hits → `addr<TAB>hits<TAB>symbol<TAB>annotation` | 05 |
+| `ExportForGreenEye.java` | Ghidra → `addr<TAB>name<TAB>size` TSV | 05 |
+| `MarkGreenEyeHits.java` | annotate functions with runtime hits + bookmarks | 05 |
+| `GreenEyeControl.java` + `greeneye.sh ghidra` | drive a live session from GhidraScripts over localhost JSON-lines | [13](13-GHIDRA-CONTROL.md) |
+
+Bridge commands: `ping`, `info`, `resolve`, `bp_set`, `bp_clear`, `bp_list`,
+`watch_set`, `run_until`, `run`, `step`, `read_mem`, `write_mem`, `read_reg`,
+`write_reg`, `call`, `snapshot`, `note`, `hits`, `export_hits`, `close`
+(13 §3). Unauthenticated by design; localhost only.
+
+## 8. MCP server tool surface
+
+`plugins/mcp_server.py` serves the active `Session` over JSON-RPC 2.0 on
+stdio (or loopback TCP) with no MCP library; read/observe by default,
+`write_mem`/`call_function` require `--allow-write`, `inject_service`
+requires `--allow-inject` ([20](20-MCP-SERVER.md)).
+
+```mermaid
+flowchart LR
+ AG["AI agent (Claude Code, opencode)"] -->|"JSON-RPC 2.0 over stdio"| M["plugins/mcp_server.py"]
+ M --> READ["read: session_info, symbols_resolve,<br/>read_mem, hexdump, snapshot, plugins_list"]
+ M --> OBS["observe: bp_set/clear, watch_set,<br/>run, run_until, step (recorded)"]
+ M --> WR["write (gated): write_mem, call_function"]
+ M --> INJ["inject (gated): inject_service"]
+ M --> TR["trace: trace_start/stop"]
+ M --> AN["analysis: analyze_current, compare_sessions, export_hits"]
+ M --> HN["harness: probe_firmware, mutation_run,<br/>portability_matrix_run"]
+ M --> CL["session: close"]
+ READ & OBS & WR & INJ & TR & AN & HN --> S["Session + Recorder"]
+```
+
+All results are one JSON text block; absent modules return a structured
+`not_available` error; stdout carries protocol frames only (20 §4, §8). Full
+argument schemas for all 24 tools are in 20 §5.
+
+## 9. Automation harness
+
+`harness/` turns the playbooks into repeatable, self-describing runs. Modules
+bootstrap the tool root and use the QEMU backend by default (18, 19, 21);
+`common.py` provides flat-image relocation, session helpers and bounded log
+reads.
+
+```mermaid
+flowchart TB
+ FW["firmware ELF / .fwimage"] --> PM & MU & CD & RE & VD & LV
+ SYM["Ghidra exports + symbol DBs"] --> PM & MU & CD & RE & VD & LV
+ STIM["stimulus / probe spec"] --> PM & MU & CD & RE & VD & LV
+ SEED["seed + budgets"] --> PM & MU & CD & RE & VD & LV
+ subgraph H["harness/"]
+ PM["portability_matrix.py R6/H4.1"]
+ MU["mutation_harness.py R5/H4.3"]
+ CD["control_discovery.py H4"]
+ RE["reach.py H4.1/H4.5"]
+ VD["version_diff.py H4.1"]
+ CR["ci_release.py"]
+ LV["value_demos/ s01..s10"]
+ end
+ VD --> CR
+ PM --> CR
+ MU --> CR
+ PM & MU & CD & RE & VD & LV & CR --> OUT["reports + sessions + findings.jsonl<br/>+ compare/ + bundle-manifest.json"]
+```
+
+| Entry point | Purpose | Outputs | Doc |
+|---|---|---|---|
+| `portability_matrix.py` | probe + symbols + fixed scenario + compare across targets; M1–M6 skeleton | `portability-report.json/.md`, `symbols/`, `sessions/`, `compare/` | 18 §1 |
+| `mutation_harness.py` | seeded one-input mutation, outcome classes, per-attempt session | `findings.jsonl`, `summary.md`, `sessions/` | 18 §2 |
+| `control_discovery.py` | sweep command/call surfaces; classify handler/default/trap/fault | `findings.jsonl`, `discovery-report.json`, `control-discovery.md` | 19 §3 |
+| `reach.py` | reachability, trigger discovery, required args and effects for one symbol | `reach-report.json/.md`, `probe.json`, sessions | 19 §4 |
+| `version_diff.py` | old vs new image: shape, symbols, corpus hints, probe, behavior, M1–M6 | `version-diff.json/.md`, `corpus/`, `compare/` | 21 §2 |
+| `ci_release.py` | version_diff → matrix → mutation smoke, then hash the bundle | `release-report.json`, `bundle-manifest.json` | 21 §4 |
+| `value_demos/run_all.py` | ten fixed scenarios (reply, counters, discovery, reach, diff, inject, version, mutation, Ghidra, evidence) | `artifacts/results.json` + per-demo JSON | 25 |
+| `common.py` | shared setup (relocate to `0x80000000`, open session, bounded logs) | library | 18 §1.2 |
+
+Harness limits are intentional: QEMU/re-hosted/demo code only; no hardware,
+guest, kernel module or existing gdbstub; bounded budgets; deterministic
+findings for a fixed seed (18 §5, 19 §8, 21 §7).
+
+## 10. Device map and lenient MMIO data flow
+
+`greeneye-device-map/1` is versioned JSON keyed by
+`(address, width, direction)` with values, hit counts and firmware PCs
+([24](24-DEVICE-MAP.md)). It records what the firmware touched first and lets
+the native interpreter step past unmodelled registers to collect the next
+ones; it feeds the H2.2 QEMU device model and the H3 profile DSL.
+
+```mermaid
+flowchart LR
+ P["greeneye probe (raw .fwimage in QEMU)"] -->|"blobs/qemu.log + qemu_first_fault"| H["device_map.py harvest"]
+ H --> MAP["greeneye-device-map/1<br/>entries + meta (bias, sha256)"]
+ MAP --> LOOK["DeviceMap.lookup<br/>zero / value / unimplemented"]
+ LOOK --> LEN["Native Machine(unknown_mmio=lenient)"]
+ LEN --> LOG["machine.mmio_log / mmio_unknown"]
+ LEN --> LEARN["learn_mmio=True: source trace entries"]
+ LEARN --> MAP
+ MAP --> PROF["docs/16 profile DSL<br/>falcon/csb/ptimer/pmu stubs"]
+ PROF --> H22["H2.2 QEMU device model"]
+ LOG --> NEXT["next unmapped wall; promote to manual"]
+ NEXT --> MAP
+```
+
+Honest scope: lenient execution proves an unmodelled access *can* be served
+deterministically; it does not model read-back, side effects, interrupts or
+timing, and a zero that keeps the firmware running is a hypothesis, not a
+silicon fact (24 §4).
+
+## 11. Workflow architecture: scenario → evidence → emulation change
+
+The canonical loop (H4.5, 06 §5, 07 P4, 25 §7). Each stage names its gate.
+
+| Stage | Action / instrument | Success criterion | Failure signal |
+|---|---|---|---|
+| 1. Scenario | state the firmware question and the smallest stimulus (one command/control/boot phase) | question maps to a symbol or surface | no observable exists → add one (probe/plugin) |
+| 2. Choose instrument | hooks/watchpoints for semantics; TCG/native trace for coverage; `run_until`/`call` for reachability; injection for new code | stop set answers the question without flooding | session too large/noisy → filter PCs, watch fields only |
+| 3. Run | fresh bounded session; fixed stimulus, seed and budgets | run completes; UART/banner liveness holds | timeout, trap loop, wedged target → capture and retry clean |
+| 4. Record | `greeneye-session/1` with snapshots and notes | events + snapshots + hashes written | missing `session.json`/empty analyzer → recording off |
+| 5. Compare | `compare` against a reference (old firmware, hardware/VFIO capture, self-diff) | deltas explained symbolically; self-diff is zero | unexplained hit/watch/write delta → investigate first |
+| 6. Decide | fill M1–M6 with the reference method (06 §5); mutation/triage for error paths | metric table improves or stays equal | metric claimed without a reference capture |
+| 7. Implement | change `open-iov-vfio` (never the simulator for a product fix); keep the stimulus | physical confirmation for behaviour changes (06 §6, 14 §7) | product change from a simulator-only observation |
+| 8. Regression | re-run step 3 with an unchanged stimulus; golden sessions/CI (`make test`, `greeneye.yml`) | profile improves or stays equal; CI green | regression → revert and re-derive |
+
+```mermaid
+flowchart LR
+ Q["Question"] --> OBS["Observe<br/>probe / hooks / watch / trace"]
+ OBS --> ORI["Orient<br/>analyze + symbol decode + snapshots"]
+ ORI --> DEC["Decide<br/>compare + M1-M6 + mutation"]
+ DEC --> ACT["Act<br/>implement in open-iov-vfio"]
+ ACT --> REG["Regression<br/>same stimulus, same budgets"]
+ REG -->|"improved / equal"| SHIP["Evidence pack<br/>sessions + report + hashes"]
+ REG -->|"regressed"| Q
+ SHIP --> OBS
+```
+
+OODA compression evidence (analog, measured) is in 25 §7: hardware cycles are
+minutes and campaigns hours/days; the green-eye path for the same question
+class is sub-second to seconds plus scripting.
+
+## 12. API surfaces by tool
+
+### 12.1 CLI (`greeneye.sh`, `greeneye/cli.py`; usage in [02](02-USAGE.md))
+
+| Command | Purpose | Not for |
+|---|---|---|
+| `info` | environment/tooling check | validating a firmware image |
+| `demo` | canned boot + hooks + watch + record on the bundled uproc | booting real GSP (use `probe`) |
+| `run <script.py>` | run a Python instrumentation session | shell pipelines (write Python) |
+| `attach` / `launch` | interactive REPL / keep a target alive by port | injecting into a live product |
+| `record` | boot and record with watches/snapshots/trace | post-processing an existing session |
+| `probe <image>` | bounded run of a raw image; first divergence report | booting the image (no TU104 model) |
+| `inject` | load/register/invoke an SDK service | shipping or hot-patching signed firmware |
+| `ghidra` | serve a live session to GhidraScripts | network exposure (localhost only) |
+| `analyze <session>` / `compare <a> <b>` | markdown report / symbolic A/B delta | proving accuracy without a reference |
+| `symbols export\|resolve` | build/inspect a symbol DB | recovering types/decompilation |
+| `plugins` | list discoverable plugins | loading untrusted code into the core |
+| `python3 -m greeneye.device_map harvest\|show\|merge\|demo` | MMIO first-touch map data | a device model or timing source |
+
+### 12.2 Python entry points (recipes in [03](03-SCRIPTING.md))
+
+| Entry point | Purpose | Not for |
+|---|---|---|
+| `ge.launch/attach`, `Session.hook/watch/run/run_until/call/step/note` | session lifecycle and instrumentation | in-process C instrumentation |
+| `Memory.read/write/hexdump/search_*/snapshot/diff/render_diff` | memory inspection and region diff | full-machine snapshots (M2+) |
+| `SymbolDB.load_elf/*_tsv/*_json`, `resolve/annotate` | address-space-aware symbols | type recovery from Ghidra |
+| `structs.schema_from_json/decode/render` | declarative struct decoding | automatic struct discovery |
+| `probes.load_spec/install` | JSON probe specs (no arbitrary code) | complex control flow (write a script) |
+| `inject.load_service/invoke_remote/invoke_via_firmware` | ABI v1 service load/invoke | product features or persistence |
+| `ghidra.build_symbol_db/export_hits/corpus_location` | Ghidra TSV in/out and corpus location | replacing Ghidra analysis |
+| `analyze.render_summary/compare_sessions/parse_qemu_faults/load_trace` | offline reports and QEMU fault parsing | live control |
+| `record.Recorder/load_events/load_meta` | session writing/reading | a database (files are the interface) |
+| `device_map.harvest_*/DeviceMap.*` | MMIO map authoring/merge | device semantics |
+| `sim_backend.launch_native`, `sim.trace.Trace/make_trace` | native backend + trace knobs | timing/concurrency claims |
+| `errors.*` (`GreenEyeError`, `RspTimeout`, ...) | typed failure surface | retrying a wedged target blindly |
+
+### 12.3 Plugin API (`register(ge)`, [04](04-PLUGINS.md))
+
+`ge` exposes live objects (`session`, `memory`, `symbols`, `recorder`),
+instrumentation (`hook`, `watch`, `on_event`, `resolve`), CLI extension
+(`add_command(name, fn, help)`, `commands`) and audit logging (`log`). A
+raising plugin is recorded as `plugin_error` and skipped. Not for: shipping
+dependencies, assuming a specific firmware, or hiding unrecorded state.
+
+## 13. Extensibility and invariants
+
+Where extension lives:
+
+* **Plugins** (`plugins/`, `GREENEYE_PLUGINS`) — runtime RE tools, harnesses,
+ environment updates, agent integrations (04).
+* **Harness modules** (`harness/`) — new sweeps/reports over the public
+ `Session`/analysis primitives; a new call surface is a `Surface` subclass
+ (19 §5.1).
+* **SDK services** (`sdk/`) — PIC blobs behind ABI v1; v2 adds an RM-core call
+ table (12 §8).
+* **Device maps/profiles** — versioned data (`greeneye-device-map/1`, the
+ `greenmachine-profile/1` DSL in 16 §7), never core special cases.
+* **MCP tools** (`plugins/mcp_server.py`) — add tools in the plugin, keep the
+ core stdlib-only (20 §10).
+
+Invariants (enforced by tests or review):
+
+1. **stdlib-only core**: `greeneye/` and `sim/` import only the standard
+ library; optional deps live in plugins/harnesses.
+2. **Session format**: `greeneye-session/1` is append-only and evolves
+ additively with migration notes (V2).
+3. **Backend parity**: every backend satisfies the same `Session` primitives,
+ so scripts, probes, plugins, recordings and comparisons survive backend
+ changes (16 §4, R0).
+4. **ABI**: blobs declare an ABI version and capability metadata; breaking
+ changes need a shim and a doc note (12 §8, V3).
+5. **Markers**: the `TOOL IN DEVELOPMENT` banner stays until the acceptance
+ list in 00 passes; never shipped, never in a guest, never a driver
+ dependency.
+6. **Evidence**: no accuracy, portability or security claim without a recorded
+ A/B session and an explicit L2/L3 fidelity statement (06 §5–6, 25 §10).
+7. **Provenance**: tracked docs never name local source trees; public
+ descriptions first, local provenance in the local notes (V6).
+
+## 14. Diagram and document index
+
+Section diagrams and their backing docs: §2 suite (00, 01, README); §3
+dispatch (01, 03); §4 backends (01 §8, 16 §4–5); §5 recording (01 §6, 09, 12
+§6, 16 §10, 17 §5); §6 injection (11, 12); §7 Ghidra (05, 13); §8 MCP (20);
+§9 harness (18, 19, 21); §10 device map (24, 16 §7); §11 OODA (06, 07 P4, 25
+§7). Roadmap keys: R3 = MCP (08), R5 = mutation (18), R6 = multi-version (18,
+21), H1.3 = RM service injection (GREEN-EYE-ROADMAP), H2.2 = QEMU device
+model, H3/M1–M6 = native simulator (16), H4 = adversarial navigation (14),
+H7 = agentic platform (15, 20).
diff --git a/debug/risc-v-gsp-rm-vm/docs/27-TOOL-AUDIT.md b/debug/risc-v-gsp-rm-vm/docs/27-TOOL-AUDIT.md
new file mode 100644
index 0000000..e208253
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/27-TOOL-AUDIT.md
@@ -0,0 +1,252 @@
+# 27 — green-eye tool audit
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!** See [00-OVERVIEW](00-OVERVIEW.md)
+> for status and [06-ACCURACY](06-ACCURACY.md) for fidelity limits.
+> Development-only: never shipped, never a driver dependency, never in a guest.
+
+Audit date: 2026-10-02. Scope: every runnable surface in
+`debug/risc-v-gsp-rm-vm/` (CLI, Python package, `sim/`, `harness/`,
+`plugins/`, `qemu/`, `ghidra/`, `rmharness/`, `sdk/`, firmware demo,
+`examples/`, `tests/`, docs). The goal is that every tool is either exercised
+somewhere, documented well enough to be unmistakable, or explicitly marked
+`not used yet`.
+
+## 0. Method (what was actually run)
+
+Environment: QEMU 8.2.2 (`qemu-system-riscv64`), Python 3.12, clang/lld.
+Every safe smoke below was executed from the tool root; device/WPR paths were
+not touched.
+
+* all 13 `./greeneye.sh <cmd> --help` parsers, `--version`;
+* `info`, `demo --backend native`, `record --backend native --trace`,
+ `analyze`, `compare`, `plugins`, `inject --backend native`;
+* `probe --instructions 400` against the real 570.144 `.fwimage`;
+* `launch` (bounded, QEMU killed afterwards) and the attach REPL path via
+ `tests/test_injection.py`;
+* `symbols export` from a synthetic Ghidra index TSV, then `symbols resolve`;
+* `run` of every `examples/scripts/*.py` as documented, plus the declarative
+ probe spec `examples/probes/demo_probes.json`;
+* `make demo`, `make sdk`, `make -C qemu` (plugin already built) and a live
+ QEMU run with `qemu/greeneye-trace.so` (8,012,129 JSONL events in ~1 s);
+* `harness/*.py --help`; `harness/value_demos/run_all.py --only s02,s05`;
+ `examples/scripts/sim_bench.py`, `sim_diff.py`;
+* `plugin --help` / MCP suite; `rmharness/*.sh --help`,
+ `rmharness/harness.sh` capture with `rmtool gpu list` (read-only `/proc`);
+ `python3 rmharness/tests/test_rmharness_plugin.py` (3 tests);
+* `python3 -m greeneye.device_map --help` and `demo` (bounded 50k
+ instructions, lenient MMIO);
+* full suite: `python3 -m unittest discover -s tests -t .` → **155 tests, OK**;
+* `python3 scripts/check-docs-links.py` from `open-iov/` → **OK, 55 files**;
+* `git check-ignore -v` for the local-only ignore patterns.
+
+Status vocabulary: `verified useful` (exercised and/or wired into
+tests/demos/docs), `not used yet` (no caller found; decision recorded),
+`needs improvement` (concrete weakness; fixed where safe), `prune candidate`
+(clearly dead; none found).
+
+## 1. Master table — `greeneye.sh` CLI
+
+| surface | purpose | API / entry points | tests | used by | status | notes/actions |
+|---|---|---|---|---|---|---|
+| `info` | environment/tooling check | `greeneye/cli.py:173`, `greeneye.sh info` | indirect (smoke) | README, 00, 02 | verified useful | ran; native sim + fwimage + CUDA names detection all correct |
+| `demo` | canned demo boot + hooks/watch/diff/trap | `cli.py:209` | smoke via `test_smoke_qemu.py` | README, 00, 02; `make demo-run` | verified useful | ran on `--backend native`; 17 hook lines, diff + trap work |
+| `run` | run a Python instrumentation script | `cli.py:272` | `test_injection.py` (REPL), `test_sim_trace.py` | 02, 03, examples scripts | needs improvement (fixed) | the exec namespace lacked `__file__`, so the documented `dump_diff.py` failed; namespace now sets `__file__`/`__name__`, doc 03 notes it |
+| `attach` | interactive REPL | `cli.py:486`, `GreenEyeRepl` | `test_injection.py:68` | README, 02 | verified useful | REPL commands all back onto tested Session APIs |
+| `launch` | start target, keep alive for attach | `cli.py:509`; `qemu/launch.sh` | smoke (bounded run) | README, 02 | verified useful | prints gdbstub endpoint; QEMU survives Ctrl-C by design |
+| `probe` | run arbitrary/real firmware, report first divergence | `cli.py:551`; `test_probe.py` unit | `test_probe.py` (ELF/flat/fault parse) | README, 00, 06, 24 (`harvest` input) | verified useful | ran vs real fwimage: first fault `fault_fetch @0x0`, recorded session evidence |
+| `record` | record a memory-activity session (+ native trace) | `cli.py:529` | `test_sim_trace.py` | README, 02, 03 | verified useful | ran with `--trace`; trace blob + watch events rendered by `analyze` |
+| `analyze` | session → symbol-annotated report | `cli.py:690`, `greeneye/analyze.py` | `test_sim_trace.py`, `test_smoke_qemu.py` | 02, 03, 05, 13, 14, 25 | verified useful | ran on recorded session |
+| `compare` | A/B recorded sessions | `cli.py:704`, `analyze.compare_sessions` | `test_sim_backend.py`, `test_value_demos.py` | 02, 03, 14, 25 | verified useful | ran demo-vs-record; correct profile deltas |
+| `symbols export\|resolve` | build/inspect symbol DB | `cli.py:711` | `test_core.py` (loaders) | 05, README | needs improvement (fixed) | bad `--db` JSON produced a traceback; `SymbolDB.load_json` now raises a clean `GreenEyeError` (exit 2) |
+| `plugins` | list discoverable plugins | `cli.py:733` | smoke | README, 04 | verified useful | lists all three bundled plugins |
+| `inject` | load + call an injected RV64 service | `cli.py:630`; `greeneye/inject.py` | `test_injection.py` | 11, 12, 14 | verified useful | ran native: direct call and firmware mailbox both `0xbeae` |
+| `ghidra` | serve session to GhidraScripts | `cli.py:660`; `greeneye/gdbridge.py` | `test_bridge.py` (full protocol) | 05, 13, 14 | verified useful | bridge protocol covered live over a socket; Java side needs Ghidra |
+
+## 2. Master table — Python API (`greeneye/*.py`)
+
+| surface | purpose | API / entry points | tests | used by | status | notes/actions |
+|---|---|---|---|---|---|---|
+| `__init__.py` | public API: `launch`, `attach`, exports | `ge.launch(backend=, trace=, ...)` | all integration tests | README, 02, 03 | verified useful | stable-ish v0.1 surface, documented |
+| `errors.py` | exception taxonomy | `GreenEyeError` + subclasses | everywhere | 03 §13 | verified useful | small and meaningful |
+| `rsp.py` | GDB-free RSP client | `RspClient`, `StopReply` | `test_rsp.py`, `test_mcp.py` | 01, 03 | verified useful | fake-stub unit tests |
+| `elf.py` | ELF64/32 reader, flat kernel writer | `load_elf`, `write_flat_kernel`, `write_relocated_elf`, `FirmwareImage` | `test_core.py` (`write_relocated_elf` test added), `test_probe.py`, `test_device_map.py` | 06, 12 | verified useful | `write_relocated_elf` had no caller; a relocation test now locks its entry/PT_LOAD shift |
+| `symbols.py` | symbol DB, address translation, TSV/JSON loaders | `SymbolDB.get/lookup/resolve/annotate/by_subsystem`, `Symbol` | `test_core.py` (`by_subsystem` + list/bad JSON tests added), `test_mcp.py`, `test_memory_record.py` | 03, 05 | verified useful | `load_json` list payloads worked by accident? no — they crashed; fixed and tested; parse errors now `GreenEyeError` |
+| `memory.py` | read/write/hexdump/search/snapshot/diff | `Memory`, `Diff` | `test_memory_record.py` | 03 §5/§7 | verified useful | full docs in 03 |
+| `structs.py` | schema-driven struct decode/render | `schema_from_json`, `Schema`, `render` | `test_core.py` | 03 §6 | verified useful | docs example matches demo schema |
+| `record.py` | durable session: events.jsonl/snapshots/summary | `Recorder`, `NullRecorder`, `load_events` | `test_memory_record.py` | 01, 02, 03 | verified useful | `rmharness/harness.sh` also appends compatible events |
+| `session.py` | hooks/bp/watchpoints/events/step/call | `Session`, `hook`, `watch`, `run`, `call` | `test_injection.py`, `test_mcp.py`, `test_bridge.py` | 01, 03, everywhere | verified useful | `Session.symbol_at` is a thin alias of `symbols.resolve` with no caller yet; docstring added, kept for API symmetry |
+| `target.py` | QEMU process lifecycle + UART/dlog | `QemuTarget`, `find_qemu`, `free_port` | `test_smoke_qemu.py`, `test_injection.py` | 01, 02 | verified useful | `launch`/`probe` both exercise it |
+| `sim_backend.py` | in-process native backend (M1) | `launch_native`, `SimClient`, `SimTarget` | `test_sim_backend.py`, `test_sim_trace.py`, `test_device_map.py`, `test_mcp.py` | README, 16, 24 | verified useful | QEMU-parity differential test |
+| `analyze.py` | report renderer, fault parser, A/B compare | `render_summary`, `compare_sessions`, `describe_fault` | `test_sim_trace.py`, `test_probe.py`, `test_smoke_qemu.py` | 02, 03, 06, 25 | verified useful | consumed by CLI and value demos |
+| `device_map.py` | versioned first-touch MMIO map + lenient policy | `python3 -m greeneye.device_map {harvest,show,merge,demo}` | `test_device_map.py` | 24 | verified useful | `demo` reproduced the documented first GSP access (lenient run 50k insns) |
+| `gdbridge.py` | JSON-lines Ghidra control server | `GhidraBridge`, `cmd_*` | `test_bridge.py` | 05, 13 | verified useful | 25 commands; protocol tested live |
+| `ghidra.py` | symbol import / hit export | `build_symbol_db`, `export_hits`, `corpus_location`, `render_ghidra_command` | `test_core.py` (TSV loaders), `test_value_demos.py` s09 (`export_hits`), `test_bridge.py` (`render_ghidra_command` test added) | 05, 13, 25 | verified useful | `render_ghidra_command` now has a test and is cited from 05 §4 |
+| `inject.py` | position-independent service load/call | `load_service`, `invoke_remote`, `invoke_via_firmware` | `test_injection.py` | 11, 12 | verified useful | both call paths run in tests and in the CLI smoke |
+| `plugins.py` | plugin discovery/API | `PluginManager`, `GreenEyeAPI` | `test_injection.py`; plugin smoke | 04, plugins/README | verified useful | bundled + `GREENEYE_PLUGINS` paths |
+| `probes.py` | declarative JSON probes | `load_spec`, `install`, `compile_condition` | `test_smoke_qemu.py`; `demo_probes.json` smoke | 03 §9, 14 | verified useful | 3 probes installed, fired as expected; no standalone runner needed (documented snippet) |
+| `cli.py` | argparse entry points (section 1) | `main`, `build_parser` | CLI smokes + integration tests | everywhere | needs improvement (fixed) | docstring now lists `launch`/`inject`/`ghidra`; `run` sets `__file__` |
+
+## 3. Master table — simulator, harness, plugins, host tooling
+
+| surface | purpose | API / entry points | tests | used by | status | notes/actions |
+|---|---|---|---|---|---|---|
+| `sim/decode.py` | RV64IMAC decoder | `decode32`, `decode16` | `test_sim_backend.py` | 16 | verified useful | compressed canonicalisation tested |
+| `sim/machine.py` | deterministic machine, UART, lenient MMIO | `Machine`, `Uart16550` | `test_sim_backend.py`, `test_sim_trace.py`, `test_device_map.py` | 16, 24 | verified useful | no wall-clock claims (06) |
+| `sim/trace.py` | native exec/access trace (R4) | `Trace` | `test_sim_trace.py` | 16, README | verified useful | trace blob rendered by `analyze` |
+| `harness/common.py` | shared harness helpers | `load_target`, `write_json`, `read_faults_window`, … | `test_harness.py`, `test_discovery.py`, `test_version_diff.py` | 18–23 | verified useful | internal, well factored |
+| `harness/ci_release.py` | release triage orchestration | `python3 harness/ci_release.py --old … --new …` | `test_version_diff.py` | 21 | verified useful | dry-run supported; manifest/mini-gate |
+| `harness/control_discovery.py` | unknown-call discovery ("Sandsifter for RM") | CLI, `--surface demo\|rm` | `test_discovery.py` | 19, 21 | verified useful | deterministic probe generation + effect observer |
+| `harness/mutation_harness.py` | bounded deterministic mutation | CLI `--iterations/--seed` | `test_harness.py` (integration, seed replay byte-identical) | 18, 20, 23 | verified useful | replayed identically in test |
+| `harness/portability_matrix.py` | multi-target portability runs | CLI `--target` (repeatable) | `test_harness.py` (self-comparison all-zero deltas) | 18, 22 | verified useful | includes probe/scenario/compare sections |
+| `harness/reach.py` | reachability answers for a target | CLI `--method auto\|stimulus\|trigger\|run\|call` | `test_discovery.py` | 19 | verified useful | emits suggested probe spec |
+| `harness/version_diff.py` | old-vs-new firmware diff | CLI `--old/--new` | `test_version_diff.py` | 21, 25 | verified useful | composes ci_release |
+| `harness/value_demos/*` | s01–s10 value validation + `run_all.py` | `python3 harness/value_demos/run_all.py [--only]` | `test_value_demos.py` | 25, MCP tools | verified useful | ran s02/s05 from the CLI; artifacts under `value_demos/artifacts/` |
+| `plugins/example_plugin.py` | plugin authoring reference | `register(ge)` | plugin smoke (`--plugin`) | 04, plugins/README | verified useful | loaded via `greeneye run --plugin` |
+| `plugins/mcp_server.py` | MCP server (stdio/loopback) | `--stdio`, `--tcp`, `--allow-write`, `--allow-inject` | `test_mcp.py` (431 lines) | 08/R3, 15, 20 | verified useful | read-only default; structured `not_available` fallbacks |
+| `plugins/rmharness_plugin.py` | REPL commands for host RM oracle | `register(ge)`; commands `rmtool/rmtop/rmq` | `rmharness/tests/test_rmharness_plugin.py` (3 tests) | 17, 23 | verified useful | write-named args refused (`rm_harness_skip`) |
+| `qemu/greeneye_trace.c` | full TCG exec/access JSONL trace | `make -C qemu`; `-plugin greeneye-trace.so,path=…` | none automated; **live-verified in this audit** | 10, 16, 14 | verified useful | 8M exec events in 1 s on the demo; vendored `qemu-plugin.h` builds out of the box |
+| `qemu/launch.sh` | one-line `greeneye launch` wrapper | `qemu/launch.sh --kernel <elf>` | smoke | qemu/README | verified useful | thin `exec` of `greeneye.sh launch` |
+| `ghidra/*.java` (Export/Mark/Control) | static/dynamic join inside Ghidra | `analyzeHeadless -postScript …` | protocol counterpart covered by `test_bridge.py`; no Ghidra installed here | 05, 13 | verified useful (static) | README documents exact command lines; Java not executed in this environment |
+| `rmharness/rmtool.sh` | RM oracle wrapper | passthrough to `tools/rmtool` | `harness.sh` capture smoke (`gpu list`) | 17, 23 | verified useful | builds tool on first use; read-only |
+| `rmharness/rmtop.sh` | dashboard wrapper | passthrough to `tools/rmtop` | `--help` | 17 | verified useful | `--once` for captures |
+| `rmharness/rmq.sh` | pre-existing rmq wrapper | passthrough to `tools/rmq` | `--help` | 17 | needs improvement (fixed) | `--help` used to fall through and open the RM; wrapper now answers help itself |
+| `rmharness/harness.sh` | capture tool run into a green-eye session | `--session DIR -- TOOL ARGS` | capture smoke (verified event + blobs) | 17, 22 | verified useful | additive recording; read-only allowlist enforced |
+| `rmharness/tests/` | plugin registration check | `python3 rmharness/tests/test_rmharness_plugin.py` | 3 tests, run and green | 17 | needs improvement (open) | not part of `tests/` discovery nor CI; kept separate by design, documented in 17/README — consider CI step when the workflow is next edited |
+| `sdk/injected_demo.c` | position-independent service SDK | `make sdk` | `test_injection.py`; `inject` smoke | 11, 12 | verified useful | direct + firmware call paths |
+| `firmware/uproc_demo/` | bundled RV64 uproc (mailbox RPC/state/trap) | `make demo` | many integration tests | README, 00, 02 | verified useful | self-test + scripted stimulus work |
+| `examples/scripts/dump_diff.py` | schema decode + snapshot diff demo | `./greeneye.sh run examples/scripts/dump_diff.py` | verified after `__file__` fix | 02 | needs improvement (fixed) | was broken by the `run` namespace gap; now prints decoded ctrl + changed bytes |
+| `examples/scripts/trace_rpc.py` | hook every RPC handler | `./greeneye.sh run …` | ran | 02, 16, 18 | verified useful | 17 hook events from the demo |
+| `examples/scripts/sim_bench.py` | native throughput benchmark | standalone CLI | ran (0.92 MIPS untraced, pure Python) | 16 | verified useful | self-bootstraps `sys.path`, works from anywhere |
+| `examples/scripts/sim_diff.py` | QEMU-vs-native differential | standalone CLI | ran: `PARITY: OK` | 16 | verified useful | exit 1 on divergence, 0 on skip |
+| `examples/probes/demo_probes.json` | declarative probe spec sample | loaded by `greeneye.probes.install` | installed + fired via `greeneye run` | 03 §9 | verified useful | 3 probes, hits `[1, 3, 0]` on the demo |
+| `tests/` (15 files) | unit + live QEMU integration | `python3 -m unittest discover -s tests -t .` | **155 tests OK** | CI (`greeneye.yml`) | verified useful | includes live QEMU, native parity, MCP, bridge, harness, value demos |
+
+## 4. Scripting API purpose review
+
+* **Entry points and lifetime.** `ge.launch()` (QEMU or `backend="native"`)
+ and `ge.attach()` return a `Session`; `close()` clears breakpoints and kills
+ owned QEMU unless `keep_alive=True`. Documented in 03 §1 and the
+ `__init__` docstring.
+* **`greeneye run` namespace.** Preloads `ge`, `session`/`s`, `memory`,
+ `symbols`, `recorder`, plus `__file__`/`__name__` (fixed here so scripts can
+ locate bundled assets; `examples/scripts/dump_diff.py` depends on it).
+ `docs/03-SCRIPTING.md` §intro now states this.
+* **Static/dynamic join.** `SymbolDB` understands ELF, Ghidra name/index/
+ export/callgraph TSVs and native JSON; `resolve()/annotate()` power every
+ report. `by_subsystem` now has a unit test; `Session.symbol_at` is an
+ untested-but-documented alias of `symbols.resolve`.
+* **Safety.** Probe conditions are whitelisted (`compile_condition`), and the
+ RSP/MCP surfaces never eval user text except through that whitelist.
+* **Errors.** All library errors derive from `GreenEyeError`; `load_json` now
+ reports file/parse failures as `GreenEyeError` instead of leaking
+ `JSONDecodeError` through the CLI.
+* **Gaps (deliberate).** No session rewind/replay API; TCG JSONL import into
+ sessions is still roadmap (R4).
+
+## 5. Plugin API purpose review
+
+* **Contract.** `register(ge)`; `ge` exposes `session`, `memory`, `symbols`,
+ `recorder`, `hook`, `watch`, `on_event`, `add_command`, `log`
+ (`greeneye/plugins.py:36`, `plugins/README.md`). A raising plugin is
+ recorded as `plugin_error` and cannot break the session.
+* **Discovery order.** `--plugin PATH` → `GREENEYE_PLUGINS` → bundled
+ `plugins/`. Documented in `plugins/README.md` and 04.
+* **Bundled plugins.**
+ * `example_plugin.py` — authoring reference; loaded in this audit.
+ * `mcp_server.py` — the AI/agent surface; tested by `tests/test_mcp.py`,
+ documented in 20. The old "MCP is planned/do not add it" notes in
+ `plugins/README.md`, 04 and 08 are now corrected to point at 20.
+ * `rmharness_plugin.py` — host oracle REPL commands; tested separately.
+* **Gaps.** Plugins cannot yet declare tool capabilities for MCP discovery
+ beyond `add_command`; `run_command` is REPL-only. Noted, not blocking.
+
+## 6. Unused tools and decisions
+
+No `prune candidate` was found: every unreferenced item was a small, coherent
+public helper. Three were cheaply wired into unit tests; one remains
+`not used yet` with a docstring. Nothing was deleted.
+
+| item | decision | rationale |
+|---|---|---|
+| `greeneye.symbols.SymbolDB.by_subsystem` | **wired**: docstring + `test_core.test_by_subsystem` | natural read-only query alongside `names()` |
+| `greeneye.elf.write_relocated_elf` | **wired**: `test_core.test_write_relocated_elf` verifies entry/PT_LOAD shift | complete ELF utility next to `write_flat_kernel` |
+| `greeneye.ghidra.render_ghidra_command` | **wired**: `test_bridge.GhidraCommandTests` + cited in 05 §4 | renders the `MarkGreenEyeHits.java` command for scripts |
+| `greeneye.session.Session.symbol_at` | `not used yet`: docstring only; kept | thin alias of `symbols.resolve`; external scripts may prefer the Session namespace |
+
+Nothing was deleted.
+
+## 7. Portability / extensibility / discoverability checks
+
+* **Portability.** Stdlib-only Python; `greeneye.sh` pins `PYTHONPATH` to the
+ tool root. Harness tools and `run_all.py` bootstrap their own `sys.path`.
+ QEMU is optional (integration tests skip cleanly); the native backend makes
+ the suite largely hardware-free. `qemu-plugin.h` is vendored so the TCG
+ plugin builds without a QEMU source tree (verified).
+* **Extensibility.** Three documented extension axes with stable contracts:
+ Python scripts (`run`, doc 03), JSON probe specs (`probes`, doc 03 §9), and
+ plugins (`register(ge)`, docs 04/20). Device behaviour is data, not code
+ (`greeneye-device-map/1`, doc 24).
+* **Discoverability.** `README.md` + `docs/00-OVERVIEW.md` doc maps were
+ completed for docs 17–27 (previously they stopped at 16); `plugins/` and
+ `harness/` are now listed in the README layout. `greeneye.sh --help` and
+ `plugins/mcp_server.py --help` are self-describing.
+* **CI.** `.github/workflows/greeneye.yml` builds SDK+demo and runs the main
+ suite on Linux; `rmharness/tests` is the only suite the workflow misses.
+
+## 8. How to tell if a tool is misbehaving (symptom → check → fix)
+
+| tool | symptom | check | fix |
+|---|---|---|---|
+| `greeneye.sh` / CLI | `greeneye error: …` on a valid path | `./greeneye.sh info` shows resolved paths; `--help` for arguments | correct the path/arg; check `GREENEYE_QEMU`/`GREENEYE_DUMPS`; bad symbol DBs now fail with a clear message |
+| `run` scripts | `NameError: __file__` (pre-fix) or names missing | `./greeneye.sh run` preloads `ge/session/s/memory/symbols/recorder/__file__`; run with `--backend native` to remove QEMU | keep scripts inside the tool tree and invoke through `greeneye.sh run`, not bare `python3` |
+| hooks/watchpoints | callback never fires | hook an obviously-hit symbol (`rpc_sum`), `record` + `analyze`; check the session event counts | re-run demo to confirm firmware self-test; ensure the target is resumed (`run`) and the symbol exists (`symbols.get`) |
+| `probe` | 0 faults reported | check `dumps/<run>/blobs/qemu.log` and `recorder.meta["qemu_fault_count"]` | expected for images that stay in mapped memory; use `--instructions` for a bounded step instead of `--duration` |
+| native backend / trace | QEMU-vs-native diff fails | `python3 examples/scripts/sim_diff.py`; `analyze` on the trace blob | divergence means a real backend bug: capture both sessions and diff `exec`/`access` counts (see 16) |
+| `inject` | service not found or call returns garbage | `make sdk`; check slot (`--slot 0..7`) and load address; hook `rpc_dispatch` | rebuild the SDK, restore the default slot, verify `dispatch` ABI (12 §) |
+| harness mutation/portability | findings not reproducible | `--seed` must be fixed; compare `findings.jsonl` byte-for-byte | if a seed diverges, the target or environment changed: capture a session and diff the baseline |
+| discovery / reach | everything "unreachable" | run `--surface demo` on the built demo first; check `--timeout/--settle` | raise settle time; confirm the stimulus hook matches the firmware's mailbox |
+| version diff / CI release | huge spurious diff | check symbol biases/TSVs (`--*-symbols`, `--bias`) and labels | fix the bias first; use `--dry-run` to inspect the plan before writing artifacts |
+| MCP server | client sees no tools / writes refused | `python3 plugins/mcp_server.py --help`; stdout must carry only protocol frames (logs go to stderr) | remove stdout prints from plugins; pass `--allow-write`/`--allow-inject` only when intended |
+| `rmharness` wrappers | `--help` opens the device (pre-fix) or permission errors | `--help` is now device-free; `rmtool gpu list/topology` read `/proc`/sysfs only | run `rmtool.sh --help` to check the build; device-table queries need the driver's permission model (17 §Not implemented) |
+| `qemu/greeneye-trace.so` | empty trace file | run `make -C qemu`; QEMU must reach the `start/end` window | widen `start/end`, set `exec=on`, and resume the target (plugin records only executed code) |
+| device map | unexpected `unimplemented` read | `greeneye.device_map show <map>`; check `bias` and schema version | harvest from a `probe` session with the same bias; use `unknown_mmio="lenient"` for first-touch exploration |
+
+## 9. Provenance discipline
+
+Tracked tool-tree docs describe components and behaviour; no non-public
+paths, filenames or source locations appear in them. The audit re-ran the
+marker scan across this tree and found none. No further action.
+
+## 10. Summary
+
+| status | count | items |
+|---|---:|---|
+| `verified useful` | 57 | all 13 CLI commands (2 with a fixed gap); all 19 package modules; all `sim/`; all `harness/` incl. value demos; 3 plugins; QEMU plugin + launch helper; Ghidra scripts (static); all `rmharness/` (1 fixed gap); SDK, demo firmware; 5 example assets; tests |
+| `not used yet` | 1 | `session.symbol_at` — docstring added, kept (section 6) |
+| `needs improvement` | 5 | `run` `__file__` namespace (**fixed**), `symbols resolve --db` traceback (**fixed**), `rmq.sh --help` device touch (**fixed**), stale doc maps/MCP/status text in README/00/04/08/14 + `plugins/README` (**fixed**), `rmharness/tests` outside main suite/CI (**open**, documented) |
+| `prune candidate` | 0 | nothing deleted |
+
+Files changed by this audit:
+
+* `greeneye/cli.py` — command docstring completed; `run` namespace sets
+ `__file__`/`__name__`.
+* `greeneye/symbols.py` — `load_json` raises `GreenEyeError` and now handles
+ list payloads; docstrings for `by_subsystem`, `nearest`.
+* `greeneye/session.py` — docstring for `symbol_at`.
+* `greeneye/ghidra.py` — docstring for `render_ghidra_command`.
+* `tests/test_core.py` — tests for list/bad JSON payloads, `by_subsystem`,
+ `write_relocated_elf`.
+* `tests/test_bridge.py` — test for `render_ghidra_command`.
+* `rmharness/rmq.sh` — device-free `--help`.
+* `README.md`, `docs/00-OVERVIEW.md` — doc maps completed (17–27), layout
+ rows for `harness/`/`rmharness/`, status-table corrections.
+* `docs/03-SCRIPTING.md` — documents `__file__`.
+* `docs/04-PLUGINS.md`, `docs/08-ROADMAP.md`, `plugins/README.md` — MCP is
+ implemented, not planned; point at 20.
+* `docs/05-GHIDRA-BRIDGE.md` — documents `render_ghidra_command`.
+* `docs/14-ADVERSARIAL-INTEROP-GUIDE.md` — capability table updated to
+ implemented harness/device-map/MCP status.
+* `docs/27-TOOL-AUDIT.md` — this document.
+
+Verification after the changes: 155/155 unit tests OK; docs-link check OK
+(55 files); every smoke in section 0 re-run green.