Skip to main content

re_ghidra_agy_hook/
hook.rs

1//! `re-ghidra-agy-hook` — the plugin's `PreInvocation` preflight.
2//!
3//! The four ways this plugin fails to start are all environmental: no
4//! `GHIDRA_INSTALL_DIR`, a directory that is not actually a Ghidra root, a JDK
5//! older than 21, and an unconfigured project. Without this hook each of them
6//! surfaces only on the first tool call, as an error from inside a JVM that
7//! never booted.
8//!
9//! Two rules govern what it may do:
10//!
11//! 1. **Silent when healthy.** It runs on every session start, so a clean
12//!    environment must produce no output at all. Anything else is noise the
13//!    user reads once and then learns to ignore.
14//! 2. **Never blocks.** It always exits 0. A preflight that can stop a session
15//!    is worse than the problem it reports — the user may not even intend to
16//!    touch Ghidra this session.
17
18use re_ghidra_mcp_agy::{load_settings, parse_java_major, preflight, Probe};
19use std::path::{Path, PathBuf};
20use std::process::Command;
21
22fn main() {
23    let mut raw = String::new();
24    let _ = std::io::Read::read_to_string(&mut std::io::stdin(), &mut raw);
25
26    // Parse the PreInvocation event payload
27    let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw) else {
28        return;
29    };
30
31    // Only run the preflight check on the first invocation to avoid spamming
32    // the user on every turn of the agent loop.
33    if let Some(inv_num) = v.get("invocationNum").and_then(|n| n.as_u64()) {
34        if inv_num > 1 {
35            println!("{}", serde_json::json!({}));
36            return;
37        }
38    }
39
40    let project_dir =
41        extract_project_dir(&v).unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
42
43    let mut cfg = load_settings(&project_dir);
44    overlay_env(&mut cfg);
45
46    let probe = Probe {
47        launcher_present: cfg
48            .ghidra_install_dir
49            .as_deref()
50            .is_some_and(|d| launcher_exists(Path::new(d))),
51        java_major: detect_java_major(),
52    };
53
54    let findings = preflight(&cfg, &probe);
55    if findings.is_empty() {
56        println!("{}", serde_json::json!({}));
57        return;
58    }
59
60    let body = format!(
61        "re-ghidra-mcp-agy is not ready to attach to a Ghidra project:\n{}",
62        findings
63            .iter()
64            .map(|f| format!("  - {f}"))
65            .collect::<Vec<_>>()
66            .join("\n")
67    );
68
69    let payload = serde_json::json!({
70        "injectSteps": [
71            {
72                "ephemeralMessage": body
73            }
74        ]
75    });
76
77    println!("{payload}");
78}
79
80fn extract_project_dir(v: &serde_json::Value) -> Option<PathBuf> {
81    // Try to get the first workspace path from the payload
82    if let Some(workspaces) = v.get("workspacePaths").and_then(|w| w.as_array()) {
83        if let Some(first) = workspaces.first().and_then(|f| f.as_str()) {
84            if !first.is_empty() {
85                return Some(PathBuf::from(first));
86            }
87        }
88    }
89    None
90}
91
92fn overlay_env(cfg: &mut ghidra_mcp::config::RawConfig) {
93    let get = |k: &str| std::env::var(k).ok().filter(|s| !s.is_empty());
94    for (slot, key) in [
95        (&mut cfg.ghidra_install_dir, "GHIDRA_INSTALL_DIR"),
96        (&mut cfg.project_dir, "GHIDRA_MCP_PROJECT_DIR"),
97        (&mut cfg.project_name, "GHIDRA_MCP_PROJECT_NAME"),
98        (&mut cfg.bootstrap_program, "GHIDRA_MCP_BOOTSTRAP_PROGRAM"),
99        (
100            &mut cfg.bootstrap_program_path,
101            "GHIDRA_MCP_BOOTSTRAP_PROGRAM_PATH",
102        ),
103        (&mut cfg.max_heap, "GHIDRA_MCP_MAX_HEAP"),
104    ] {
105        if let Some(v) = get(key) {
106            *slot = Some(v);
107        }
108    }
109}
110
111fn launcher_exists(install_dir: &Path) -> bool {
112    let support = install_dir.join("support");
113    support.join("analyzeHeadless.bat").exists() || support.join("analyzeHeadless").exists()
114}
115
116fn detect_java_major() -> Option<u32> {
117    let out = Command::new("java").arg("-version").output().ok()?;
118    parse_java_major(&String::from_utf8_lossy(&out.stderr))
119        .or_else(|| parse_java_major(&String::from_utf8_lossy(&out.stdout)))
120}