Thomas Piacentini
← Atelier

Chapitre

openbinary and the headless bridge

Texte original · anglais

open_binary binds one target to one provider without a Ghidra import, and the first Ghidra-backed call pays for a headless import, auto-analysis, and a private socket.

Your agent opened ledger-arm64-O1 over MCP in under two seconds and then waited several seconds for list_procedures. Nothing was wrong. The open call did not import the file into Ghidra. The first question did, and every question after it came back in milliseconds.

When you finish this section, you can read the server log of a session and name the process that the first query started. You can also predict which calls will be slow.

open_binary

the open_binary exchangejsonFull source
"method": "tools/call",
"params": {
  "name": "open_binary",
  "arguments": {
    "path": "$CAPTURE/fixtures/ledger-arm64-O1",
    "provider_id": "ghidra"
  }
}
…
"structuredContent": {
  "result": {
    "path": "$CAPTURE/fixtures/ledger-arm64-O1",
    "format": "mach-o",
    "kind": "executable",
    "sha256": "01b5d08b…",
    "architecture": "arm64"
  }
}
The request and the result. The text copy of the result is cut by the capture kit.
Original source
title: the open_binary exchange
source: capture/out/10-mcp-03-open-binary-O1.json
lang: json
note: The request and the result. The text copy of the result is cut by the capture kit.
---
"method": "tools/call",
"params": {
  "name": "open_binary",
  "arguments": {
    "path": "$CAPTURE/fixtures/ledger-arm64-O1",
    "provider_id": "ghidra"
  }
}
…
"structuredContent": {
  "result": {
    "path": "$CAPTURE/fixtures/ledger-arm64-O1",
    "format": "mach-o",
    "kind": "executable",
    "sha256": "01b5d08b…",
    "architecture": "arm64"
  }
}

The handler calls session.open with the path and the provider id, then returns five fields: path, format, kind, sha256, and architecture. To fill them rea reads the magic bytes: 7f 45 4c 46 for ELF, MZ for DOS and PE, and four Mach-O values. It hashes the whole file in 64 KiB chunks. A file with no header needs an explicit format. With format: "raw-binary" you also give architecture and entry_point, and the result adds entry_point. rea refuses a WebAssembly module here and sends it to inspect_wasm_artifact. For every other target, rea selects the provider you asked for, and binary_session records that choice.

The selection waits for the provider's readiness checks. On the capture Mac, the server log shows about 1.6 s for the first open_binary and about 0.4 s for the second. The design docs allow the wait: a readiness check can wait for the commands that it owns. Release 6.5.0 answered the same calls in 36 ms and 1 ms. Neither version imports the target at this step.

the binding that binary_session reportsjsonFull source
"open": true,
…
"analysis_provider_binding": {
  "provider": {
    "id": "ghidra",
    "name": "Ghidra",
    "version": "12.1.4"
  },
  "selection_source": "request",
  "analysis_profile": {
    …
    "digest": "3fc72c9b…"
  }
},
…
"server_identity": {
  "package": {
    "name": "rea-agents",
    "version": "6.7.0",
    …
  },
  …
  "sdk": {
    "server": "2.3.1",
    …
  },
  "negotiated_protocol_version": "2025-06-18",
  …
Cut to the provider binding and the server identity. The capabilities, candidates, and tool availability lists are cut.
Original source
title: the binding that binary_session reports
source: capture/out/10-mcp-04-binary-session.json
lang: json
note: Cut to the provider binding and the server identity. The capabilities, candidates, and tool availability lists are cut.
---
"open": true,
…
"analysis_provider_binding": {
  "provider": {
    "id": "ghidra",
    "name": "Ghidra",
    "version": "12.1.4"
  },
  "selection_source": "request",
  "analysis_profile": {
    …
    "digest": "3fc72c9b…"
  }
},
…
"server_identity": {
  "package": {
    "name": "rea-agents",
    "version": "6.7.0",
    …
  },
  …
  "sdk": {
    "server": "2.3.1",
    …
  },
  "negotiated_protocol_version": "2025-06-18",
  …

selection_source: "request" says the agent chose Ghidra. The profile digest is the one every Ghidra record of this target carries. The docs state the limit of the open call in one sentence:

Original source
label: what open_binary does not do
source: mcp §Ghidra first-query deadlines and recovery
---
`open_binary` picks the target and binds the provider. It does not prove that Ghidra has finished the import of the target.

The first Ghidra-backed call

six calls from the session indextextFull source
03  tools/call open_binary              ok     -> 10-mcp-03-open-binary-O1.json  (…)
04  tools/call binary_session           ok     -> 10-mcp-04-binary-session.json  (…)
05  tools/call list_procedures          ok     -> 10-mcp-05-list-procedures.json  (…)
06  tools/call procedure_assembly       ok     -> 10-mcp-06-procedure-assembly-grade.json  (…)
07  tools/call xrefs                    ok     -> 10-mcp-07-xrefs-grade.json  (…)
08  tools/call list_strings             ok     -> 10-mcp-08-list-strings.json  (…)
Lines 03 to 08 of the index. The time in parentheses is measured by the capture client around each call, changes on every run, and is cut.
Original source
title: six calls from the session index
source: capture/out/10-mcp-session.txt
lang: text
note: Lines 03 to 08 of the index. The time in parentheses is measured by the capture client around each call, changes on every run, and is cut.
---
03  tools/call open_binary              ok     -> 10-mcp-03-open-binary-O1.json  (…)
04  tools/call binary_session           ok     -> 10-mcp-04-binary-session.json  (…)
05  tools/call list_procedures          ok     -> 10-mcp-05-list-procedures.json  (…)
06  tools/call procedure_assembly       ok     -> 10-mcp-06-procedure-assembly-grade.json  (…)
07  tools/call xrefs                    ok     -> 10-mcp-07-xrefs-grade.json  (…)
08  tools/call list_strings             ok     -> 10-mcp-08-list-strings.json  (…)
the same calls in the server logjsonlFull source
{"level":30,"time":…,"application":"rea","mode":"mcp","layer":"server","tool":"open_binary","durationMs":…,"status":"ok","msg":"MCP tool execution completed"}
{"level":30,"time":…,"application":"rea","mode":"mcp","layer":"server","tool":"list_procedures","durationMs":…,"status":"ok","msg":"MCP tool execution completed"}
{"level":30,"time":…,"application":"rea","mode":"mcp","layer":"server","tool":"procedure_assembly","durationMs":…,"status":"ok","msg":"MCP tool execution completed"}
{"level":30,"time":…,"application":"rea","mode":"mcp","layer":"server","tool":"xrefs","durationMs":…,"status":"ok","msg":"MCP tool execution completed"}
{"level":30,"time":…,"application":"rea","mode":"mcp","layer":"server","tool":"list_strings","durationMs":…,"status":"ok","msg":"MCP tool execution completed"}
The first five lines of the stderr of rea mcp. durationMs is measured inside the server. The time and durationMs values change on every run and are cut.
Original source
title: the same calls in the server log
source: capture/out/10-mcp-server.log
lang: jsonl
note: The first five lines of the stderr of rea mcp. durationMs is measured inside the server. The time and durationMs values change on every run and are cut.
---
{"level":30,"time":…,"application":"rea","mode":"mcp","layer":"server","tool":"open_binary","durationMs":…,"status":"ok","msg":"MCP tool execution completed"}
{"level":30,"time":…,"application":"rea","mode":"mcp","layer":"server","tool":"list_procedures","durationMs":…,"status":"ok","msg":"MCP tool execution completed"}
{"level":30,"time":…,"application":"rea","mode":"mcp","layer":"server","tool":"procedure_assembly","durationMs":…,"status":"ok","msg":"MCP tool execution completed"}
{"level":30,"time":…,"application":"rea","mode":"mcp","layer":"server","tool":"xrefs","durationMs":…,"status":"ok","msg":"MCP tool execution completed"}
{"level":30,"time":…,"application":"rea","mode":"mcp","layer":"server","tool":"list_strings","durationMs":…,"status":"ok","msg":"MCP tool execution completed"}

The capture kit records the pattern in its README. The first Ghidra-backed call after open_binary takes 5 to 7 s. Every later call on the same target takes under 100 ms. The difference is the Ghidra start. GhidraClient.callTool awaits start() before it queues any request, so the first call pays for the whole sequence in #startWithin:

  1. Create a private runtime root named rea-ghidra- and a random suffix under the session root.
  2. Allocate the endpoint, a Unix socket on macOS and Linux.
  3. Copy the target into the root as target-01b5d08b08fb.bin, hashing while copying, and refuse a digest mismatch.
  4. Mint a 32-byte random token and a run id.
  5. Write session.json with transport, endpoint_path, token, run_id, target_sha256, provider_version, and profile_digest.
  6. Start Java and wait for the bridge to accept a connection.

Headless import: a Ghidra run with no window that imports one file into a project and runs the default analyzers. On macOS and Linux, rea does not run the analyzeHeadless shell script. It asks Ghidra's LaunchSupport.jar for the environment and the JVM arguments, then runs java itself. The heap is -Xmx2G unless GHIDRA_HEADLESS_MAXMEM or GHIDRA_MAXMEM is set. The command adds -Djava.awt.headless=true and names the class ghidra.Ghidra with ghidra.app.util.headless.AnalyzeHeadless.

The docs give the reason for the direct start. On Apple platforms the shell wrappers hide their environment from ownership checks. With the wrappers in place, rea could not cancel the process group safely during startup {{install §Ghidra}}.

The headless arguments come in one fixed order:

  • the project root and the project name rea-project
  • -import with the copied target
  • -readOnly and -deleteProject
  • -log and -scriptlog into the runtime root, and -scriptPath at the bridge directory
  • -postScript ReaGhidraNoReturnFix.java
  • -postScript ReaGhidraBridge.java with the path of session.json, last

Some targets and settings need more arguments. A raw binary, a DOS file, or a language override adds a loader and a processor. Seeds and disabled analyzers add pre-scripts. The fixtures of this manual need none of them.

Fig. 4.1one open and three questions over MCPsequence
The open call returns after the readiness checks with no import, the first question waits seconds for the headless import, and the next takes under 100 ms.
The open call returns after the readiness checks with no import, the first question waits seconds for the headless import, and the next takes under 100 ms. Time runs down. Solid arrows are calls, dashed arrows are replies. The violet lanes are rea's Ghidra client and the Java bridge inside analyzeHeadless. The times are the ranges that the kit notes record. From capture/out/10-mcp-server.log and 10-mcp-session.txt.

Original SVG

Original source
id: fig-4-1
kind: sequence
title: one open and three questions over MCP
claim: The open call returns after the readiness checks with no import, the first question waits seconds for the headless import, and the next takes under 100 ms.
caption: Time runs down. Solid arrows are calls, dashed arrows are replies. The violet lanes are rea's Ghidra client and the Java bridge inside analyzeHeadless. The times are the ranges that the kit notes record. From capture/out/10-mcp-server.log and 10-mcp-session.txt.

The bridge on the Java side

ReaGhidraBridge.java is a Ghidra script that runs after the import and the analyzers. Its run() reads session.json and deletes it, then requires an imported program. It checks that the Ghidra version equals provider_version and compares the program's own SHA-256 with target_sha256 in constant time. It then ends the script transaction, initializes the decompiler, and serves. On a Unix socket it binds the path, sets the mode to rw-------, and accepts one client.

Each request is one JSON line, { id, token, method, params }. Each reply is { id, ok: true, result } or { id, ok: false, error: { code, message } }, and rea parses it with a strict schema. list_procedures was request number 2 on that socket, after the ping of the handshake, and it took the Java side a few milliseconds. The seconds before it were the import and the analyzers.

Fig. 4.2the private runtime root of one sessiontree
One session owns one directory with the project, the hashed target copy, the descriptor, the socket, and the logs, and close_binary deletes it.
One session owns one directory with the project, the hashed target copy, the descriptor, the socket, and the logs, and close_binary deletes it. Read top to bottom. Each entry names the file or directory and what writes it. From src/ghidra/GhidraLauncher.ts and src/ghidra/GhidraClient.ts at rea 6.7.0, and capture/out/05-analyze-arm64-O1.json for the document name.

Original SVG

Original source
id: fig-4-2
kind: tree
title: the private runtime root of one session
claim: One session owns one directory with the project, the hashed target copy, the descriptor, the socket, and the logs, and close_binary deletes it.
caption: Read top to bottom. Each entry names the file or directory and what writes it. From src/ghidra/GhidraLauncher.ts and src/ghidra/GhidraClient.ts at rea 6.7.0, and capture/out/05-analyze-arm64-O1.json for the document name.

Deadlines and the close

rea allows 330,000 ms for the start, set by REA_GHIDRA_STARTUP_TIMEOUT_MS. On the capture Mac the start took 5 to 7 s, and the client limit is the one to watch. The pinned client SDK 2.3.1 waits 60,000 ms by default, so it can cancel before the REA startup deadline {{mcp §Ghidra first-query deadlines and recovery}}.

The docs record about 64 s on a Linux fixture and about 163 s on Windows. Set the client deadline from your own first run on your own machine.

close_binary takes a fraction of a second in the log. The client sends shutdown to the bridge with a 10 s limit, the bridge disposes the decompiler, and -deleteProject removes the project. The next open_binary on ledger-arm64-O2 took about 0.4 s. Its first analyze_function again took seconds, because every new provider session imports the target again into a new temporary project {{mcp §Ghidra first-query deadlines and recovery}}.

Original source
- Expect the first Ghidra-backed call after `open_binary` to take seconds and the rest milliseconds.
- Set the MCP client deadline from a measured cold start on your own machine.
- Ask every question you have before `close_binary`, because the project is deleted.
- Read `durationMs` in the server log to separate rea time from Ghidra time.

Sources: capture/out/10-mcp-03-open-binary-O1.json, capture/out/10-mcp-04-binary-session.json, capture/out/10-mcp-session.txt, capture/out/10-mcp-server.log, capture/out/05-analyze-arm64-O1.json, capture/README.md; rea 6.7.0 at 27dc42d: src/server/registerSessionTools.ts, src/contracts/sessionLifecycleInputs.ts, src/domain/binaryTarget.ts, src/application/BinaryTargetResolver.ts, src/ghidra/GhidraClient.ts, src/ghidra/GhidraTransport.ts, src/ghidra/GhidraTargetSnapshot.ts, src/ghidra/GhidraLauncher.ts, src/ghidra/GhidraJavaLaunch.ts, src/ghidra/GhidraClientWire.ts, src/ghidra/protocol.ts, src/config/providerStartupTimeout.ts, src/application/binary/AnalysisProviderRegistry.ts, bridge/ghidra/ReaGhidraBridge.java; rea docs/mcp-contracts.md, rea docs/installation.md, rea docs/tool-design.md; the 6.5.0 recording of capture/out/10-mcp-server.log in commit 99d70723c of this repository

Code et livrables 1
Ce que vous en retenez

Enregistré dans votre carnet sur cet appareil.

Sources et limites

Les repères de lecture sont déclaratifs. Les résultats de tests sont enregistrés séparément ; ils valent pour les cas exécutés.

AI Engineering from Scratch · 3f9715c · MIT

Télécharger ce fichier ↓