Per-agent x64dbg session through debugger()/debugger_tools()

Harbor starts the x64dbg-automate MCP server (stdio) on first use and keeps
it for the life of the agent's Harbor process; the debuggee is terminated
when that process exits. Configured by debugger_command/_path/_temp in
[reverse].
This commit is contained in:
KeelrLeek committed 2026-10-05 12:44:25 -05:00
1 parent 7ed914ebdb
commit 584fabeb74
5 files changed
+175 -4

No files matched your search

+96
View File
@@ -356,3 +356,99 @@ class Ghidra:
if result.get("isError"):
raise HarborError(text.strip() or f"Ghidra refused {tool}")
return re.sub(r"^\[Context\] Operating on: [^\n]*\n+", "", text)
class Debugger:
"""A per-agent x64dbg session through the x64dbg-automate MCP server (stdio), started on first use and kept for
the life of this Harbor process, so breakpoints and the paused game survive between calls. Configured by
`debugger_command`, `debugger_path` (x32dbg.exe) and `debugger_temp` in the [reverse] table. The plugin and the
client find each other through a session file in TEMP, so both get the same TEMP."""
def __init__(self, config):
config = config or {}
self.command = config.get("debugger_command")
if not self.command:
raise HarborError("this project has no debugger_command in its [reverse] table")
self.env = dict(os.environ)
if config.get("debugger_path"):
self.env["X64DBG_PATH"] = str(config["debugger_path"])
if config.get("debugger_temp"):
self.env["TEMP"] = self.env["TMP"] = str(config["debugger_temp"])
self.proc = None
self.counter = 0
self.lock = __import__("threading").Lock()
def _start(self):
import atexit
import queue
import subprocess
import threading
command = self.command if isinstance(self.command, list) else [self.command]
self.proc = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, env=self.env, text=True, encoding="utf-8",
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
self.answers = queue.Queue()
def read(stream, answers):
for line in stream:
if line.strip():
try:
answers.put(json.loads(line))
except ValueError:
pass
answers.put(None)
threading.Thread(target=read, args=(self.proc.stdout, self.answers), daemon=True).start()
self._rpc("initialize", {"protocolVersion": "2025-06-18", "capabilities": {},
"clientInfo": {"name": "harbor", "version": "2"}})
self._send({"jsonrpc": "2.0", "method": "notifications/initialized"})
atexit.register(self.close)
def _send(self, body):
self.proc.stdin.write(json.dumps(body) + "\n")
self.proc.stdin.flush()
def _rpc(self, method, params, timeout=120):
import queue
self.counter += 1
self._send({"jsonrpc": "2.0", "id": self.counter, "method": method, "params": params})
while True:
try:
answer = self.answers.get(timeout=timeout)
except queue.Empty:
raise HarborError(f"the debugger did not answer {method} within {timeout}s") from None
if answer is None:
self.proc = None
raise HarborError("the debugger's MCP server exited")
if answer.get("id") == self.counter:
if "error" in answer:
raise HarborError(f"debugger: {answer['error'].get('message', answer['error'])}")
return answer.get("result")
def tools(self):
with self.lock:
if self.proc is None or self.proc.poll() is not None:
self._start()
return [{"name": t["name"], "description": t.get("description", ""),
"arguments": (t.get("inputSchema") or {}).get("properties", {}),
"required": (t.get("inputSchema") or {}).get("required", [])}
for t in self._rpc("tools/list", {})["tools"]]
def call(self, tool, arguments=None, timeout=300):
with self.lock:
if self.proc is None or self.proc.poll() is not None:
self._start()
result = self._rpc("tools/call", {"name": tool, "arguments": arguments or {}}, timeout=timeout)
text = "\n".join(c.get("text", "") for c in result.get("content", []) if isinstance(c, dict))
if result.get("isError"):
raise HarborError(text.strip() or f"the debugger refused {tool}")
return text
def close(self):
"""End the debuggee and the debugger with the Harbor process, so no paused game is left behind."""
if self.proc and self.proc.poll() is None:
try:
self._rpc("tools/call", {"name": "terminate_session", "arguments": {}}, timeout=15)
except HarborError:
pass
self.proc.terminate()
self.proc = None