diff --git a/docs/superpowers/specs/2026-07-03-runtime-observation-mcp-design.md b/docs/superpowers/specs/2026-07-03-runtime-observation-mcp-design.md new file mode 100644 index 0000000..6d261d8 --- /dev/null +++ b/docs/superpowers/specs/2026-07-03-runtime-observation-mcp-design.md @@ -0,0 +1,153 @@ +# Havoc Runtime-Observation MCP -- Design Spec + +**Date:** 2026-07-03 +**Status:** approved by user, ready for implementation planning + +## Why this exists + +Static Ghidra decompilation of `HAVOC_NOCD.EXE` hit a wall investigating the alt-tab-while- +not-paused "resets to menu" bug (see `docs/FUNCTIONS.md` "Alt-tab / focus-loss investigation"). +The state machine was confirmed correct by reading code; a candidate stale-object-pointer +hypothesis was ruled out by reading code; but the actual frame-render function was never +located, and the bug's real cause could not be determined from decompiled code alone. This is +the trigger condition noted in project memory for building runtime-observation tooling: when +static analysis can't answer a question, the next tool is watching the program actually run. + +This spec covers **one** subsystem: an MCP server that lets an agent (this Claude Code +session, or a future one) attach a debugger to the running `HAVOC_NOCD.EXE` process, set +breakpoints/watchpoints, read/write memory and registers, and capture screenshots -- turning +"we can't tell what this does without running it" into an actual, drivable capability. + +Two related ideas were raised and explicitly deferred, not designed here: +- An MCP/interaction layer for a future Godot remaster port (blocked: the Godot project is + still empty). +- A local-LLM agent on the muninn/sif cf-orch GPU nodes that plays the game autonomously (a + later automation layer that would sit on top of whichever MCP exists by then). + +## Environment + +- **Caspian** (`10.1.10.228`) is the Windows machine that actually runs the game, via + dgVoodoo2. It is the user's personal machine, not a CircuitForge/cf-orch homelab node. +- Caspian and the Linux RE box share the same NFS/SMB-mounted storage already used all + session for Ghidra work (`Z:\Development\devl\Havoc` on Windows, `/Library/Development/ + devl/Havoc` on Linux) -- this already-proven bridge is why a new network/file-sharing + mechanism isn't needed for anything except the MCP protocol traffic itself. +- Claude Code can also run natively on Caspian via PowerShell (with SMB access to the same + share). This means the debug agent and MCP server can be built and iterated on directly by + an agent running on Caspian, rather than authored blind from Linux and round-tripped through + manual testing. +- There is existing precedent for exactly this kind of debugging: `tools/dbg_path.py`, a pure + `ctypes` Win32 debugger written during the original boot-crash investigation. It already + proves out `CreateProcess` with `DEBUG_ONLY_THIS_PROCESS`, `WaitForDebugEvent`, and reading + WOW64 (x86-on-x64) thread context -- including a `WOW64_CONTEXT` struct that already defines + `Dr0`-`Dr7` (the x86 hardware debug registers), which data breakpoints need. No admin rights + or third-party tooling (WinDbg/cdb) were required. +- **Future direction (informs the design, not required now):** the user intends to eventually + port Havoc to run under Wine on Heimdall (a CircuitForge homelab node), eliminating the need + for a separate Windows box entirely. Wine implements the same Win32 debug APIs `dbg_path.py` + already uses, so the debug agent's core logic should not need rewriting for that move -- + only its host changes (Wine-on-Linux instead of native Windows). + +## Architecture + +Two separate processes on Caspian, both started manually alongside the game (no auto-launch): + +``` +Claude Code ──MCP over HTTP──▶ MCP server (0.0.0.0:, LAN-reachable) + (this or a │ + future session) │ localhost only + ▼ + Debug agent (127.0.0.1:) + (extends tools/dbg_path.py) + │ ctypes Win32 debug APIs + ▼ + HAVOC_NOCD.EXE (via dgVoodoo2) +``` + +- **Debug agent** owns all Win32-specific logic: the `ctypes` debug loop, breakpoints, + watchpoints, memory/register access, screenshot capture. Exposes a local JSON-over-TCP API + on `127.0.0.1` only -- never LAN-exposed directly. +- **MCP server** is a thin translation layer: implements the MCP protocol (Streamable HTTP + transport) and turns each MCP tool call into a call to the local debug agent. No debugging + logic or state of its own beyond what it forwards. + +This split is deliberate for the future Wine-on-Heimdall move: only the debug agent needs to +run inside Wine (the only piece touching real Win32 debug APIs against the game process). The +MCP server could then run as a normal native Linux process on Heimdall, talking to the +Wine-hosted agent over the same local API -- no MCP/HTTP stack needs to work inside Wine at +all. Only the debug agent's host changes; its design and the MCP server's design do not. + +## Components and tool surface + +**Debug agent** (`tools/havoc_debug_agent.py`, extends `tools/dbg_path.py`): + +| Operation | Purpose | +|---|---| +| `launch(path)` | Start the game under the debugger (or attach if already running) | +| `set_breakpoint(addr, condition=None)` | Software (`INT3`) breakpoint at a VA. Optional `condition` is a small declarative check evaluated on hit -- e.g. `{register: "eax", op: "!=", value: "0x47"}` or `{memory: "0x47eb48+0x18", size: 4, op: "!=", value: "0x47"}`. If false, silently resume; if true, actually stop and report | +| `set_watchpoint(addr, size, mode)` | Hardware data breakpoint (`read`/`write`/`readwrite`) using `DR0`-`DR3`/`DR7`. **Hard limit: 4 concurrent** (x86 has exactly 4 hardware debug address registers) -- callers must be told this limit explicitly, not discover it via a silent failure | +| `remove_breakpoint(addr)` / `remove_watchpoint(addr)` | | +| `list_breakpoints()` / `list_watchpoints()` | | +| `continue_execution(timeout)` | Resume until a breakpoint/watchpoint hits, the process exits, or `timeout` elapses (returns "still running" in that case -- never blocks forever) | +| `read_memory(addr, len)` / `write_memory(addr, bytes)` | | +| `get_registers()` | Full WOW64 register set | +| `screenshot()` | Capture the game window | +| `get_status()` | `running` / `stopped-at-breakpoint` (which one, why) / `exited` (with exit code) | + +**MCP server** (`tools/havoc_mcp_server.py`): one MCP tool per debug-agent operation above, +plus `list_breakpoints`/`list_watchpoints` exposed directly. Binds `0.0.0.0:` on +Caspian. Requires a shared-secret bearer token (see Security) on every request. + +## Data flow (worked example: the alt-tab bug) + +1. Agent calls `launch` (or the game is already running and this attaches to it) +2. Agent calls `set_watchpoint` on whatever memory holds current game/menu state, mode=write +3. User alt-tabs away and back on Caspian +4. The watchpoint trips the moment anything writes that memory; the debug agent reports which + instruction did it +5. Agent calls `get_registers()` and `read_memory()` around that address, plus `screenshot()` + to correlate with what's visually on screen at that instant +6. Agent calls `continue_execution()` to keep running, or sets a new breakpoint based on what + was just learned, repeating until the actual cause is found + +## Error handling + +- `continue_execution` requires a timeout; never blocks indefinitely +- Reads/writes to unmapped memory return a clear error, not a debug-agent crash +- Process exit (`EXIT_PROCESS_DEBUG_EVENT`) is detected and reported with exit code, not + treated as a hang or silent disconnect +- All tool calls are serialized (single lock) -- one game process, no concurrent-call races +- Requesting a 5th concurrent watchpoint returns a clear "limit is 4" error + +## Security + +The MCP server is LAN-reachable and exposes real remote influence over a running process +(memory read/write, execution control). Even on a trusted home LAN, this needs a lightweight +shared-secret bearer token (env var on Caspian, matching value in the MCP client config) so +an arbitrary other device on the LAN can't drive it. Nothing heavier than that is warranted +(no TLS, no rate limiting, no user accounts) -- this is a personal single-user dev tool, not a +production or multi-tenant system, and adding more would be overengineering for what it is. + +## Testing + +No meaningful way to unit-test real OS debug APIs against a live GUI game in isolation, so +validation is a manual acceptance checklist instead of an automated test suite: + +1. Breakpoint at a known-good, already-understood address (e.g. `busywait_frame_limiter_60hz` + @ `0x40C8E0`) -- confirm register/memory values at the stop match what Ghidra's decompile + predicts. +2. Conditional breakpoint that's initially false (game running normally) then becomes true + (trigger the condition) -- confirm it only stops on the true case. +3. Watchpoint on `DAT_0047c48c` (the delta-time baseline, already fully understood from the + FPS-coupling investigation) -- confirm it fires exactly when + `compute_frame_delta_ticks_60hz_min1_clamp` writes it, and not otherwise. +4. Screenshot capture during a real gameplay session, confirmed to produce a viewable image. +5. End-to-end: reproduce the alt-tab bug live using the worked data-flow example above, and + confirm the tooling actually surfaces new information the static analysis couldn't. + +## Build approach + +Since Claude Code can run natively on Caspian via PowerShell with SMB access to the same +share, the recommended build path is to have an agent running there write and iterate on both +`havoc_debug_agent.py` and `havoc_mcp_server.py` directly against the real environment, rather +than authoring them blind from Linux and relying on manual round-trip testing by the user.