mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
docs: record runtime and release constraints
This commit is contained in:
1 parent
e32906afb7
commit
62ede515b7
24 files changed
+206
-57
No files matched your search
@@ -2,15 +2,17 @@
|
||||
|
||||
## Project Structure & Module Organization
|
||||
|
||||
The server is a layered ESM TypeScript application. Dependencies flow inward: `domain` ← `contracts` ← `hopper` ← `application` ← `server` ← `main`.
|
||||
REA is a layered ESM TypeScript application. Dependencies flow inward: `domain` ← `contracts` ← `hopper` ← `application` ← `server` ← adapters.
|
||||
|
||||
- `src/main.ts`: composition root. Parses config, wires the Hopper client, starts the stdio MCP transport, and owns process-lifetime shutdown.
|
||||
- `scripts/rea.mjs`: executable dispatcher. Routes only bare `mcp` and `--mcp` to the production stdio server; Incur handles registration utilities and one-shot commands.
|
||||
- `src/main.ts`: MCP adapter. Parses config, wires the shared session runtime, starts stdio transport, and owns process-lifetime shutdown.
|
||||
- `src/cli.ts`: one-shot CLI adapter for setup, diagnostics, analysis, and decompilation.
|
||||
- `src/config.ts`: Zod-validated parsing of environment configuration into `AppConfig`.
|
||||
- `src/domain/`: pure, side-effect-free modules. `errors.ts` (tagged error algebra), `result.ts` (`Result`/`ok`/`err`), `hopperValues.ts` (boundary parsers for Hopper JSON), `symbolAnalysis.ts` (Swift/ObjC name parsing).
|
||||
- `src/contracts/`: caller-visible tool schemas. `toolContracts.ts` declares the 31 official-proxy + 8 enhanced tool inventory; `enhancedInputs.ts` holds Zod input schemas for enhanced tools.
|
||||
- `src/contracts/`: caller-visible schemas for 31 direct, 8 enhanced, and 3 session tools; `enhancedInputs.ts` owns enhanced input parsing.
|
||||
- `src/hopper/`: Hopper launch and Unix-socket protocol mechanics. `BridgeLauncher.ts` spawns the Hopper app with the in-process bridge, `HopperClient.ts` correlates request/response over the socket with timeouts and cancellation, `protocol.ts` frames bridge messages.
|
||||
- `bridge/hopper_bridge.py`: runs inside Hopper and adapts declared operations to Hopper's public Python API. Hopper's bundled MCP server is not used.
|
||||
- `src/application/`: enhanced workflows. `HopperToolPort.ts` defines the port the official tools proxy through; `EnhancedTools.ts` implements the 8 RE-specific tools.
|
||||
- `src/application/`: shared CLI/MCP session composition, setup and diagnostics, and the 8 enhanced workflows.
|
||||
- `src/server/`: MCP request translation. `createServer.ts` assembles the MCP server, `registerOfficialTools.ts`/`registerEnhancedTools.ts` register each tool set, `toolResult.ts` maps `Result` values to MCP content.
|
||||
- `tests/`: Vitest suite. `tests/fixtures/` holds the fake launcher and fake Hopper bridge used as production seams.
|
||||
- `scripts/verify-real-hopper.mjs`: real-Hopper end-to-end verifier.
|
||||
@@ -25,16 +27,17 @@ The server is a layered ESM TypeScript application. Dependencies flow inward: `d
|
||||
- `npm run lint`: apply type-aware ESLint rules.
|
||||
- `npm run format:check`: verify Prettier formatting.
|
||||
- `npm run check`: run typecheck, lint, format:check, and tests.
|
||||
- `npm run verify:hopper`: build and run the real-Hopper verifier (requires a binary).
|
||||
- `npm run verify:hopper`: build and run the real-Hopper verifier with two distinct binaries.
|
||||
- `npm run verify:package`: pack and test the CLI, setup transaction, skill, and 42-tool target-free MCP server in an isolated environment.
|
||||
- `npm run config:print -- /path/to/binary`: print an MCP server config with absolute paths.
|
||||
- `HOPPER_TARGET_PATH=/path/to/binary npm start`: launch Hopper and run the built stdio MCP server.
|
||||
|
||||
## Configuration & Environment Variables
|
||||
|
||||
- `HOPPER_TARGET_PATH` (required): absolute path to the binary or `.hop` database to analyze.
|
||||
- `HOPPER_TARGET_PATH` (optional): absolute initial binary or `.hop` target. Target-free MCP sessions use `open_binary` instead.
|
||||
- `HOPPER_LAUNCHER_PATH` (optional): override the Hopper executable path (defaults to `/Applications/Hopper Disassembler.app/Contents/MacOS/hopper`).
|
||||
- `HOPPER_TARGET_KIND` (optional, default `executable`): `executable` or `database` (for `.hop` files).
|
||||
- `HOPPER_LOADER_ARGS_JSON` (optional): JSON array of strings passed to the Hopper launcher before the target, e.g. `["-l","Mach-O","--aarch64"]`.
|
||||
- `HOPPER_TARGET_KIND` (optional, default `executable`): startup kind for `HOPPER_TARGET_PATH`; dynamic targets are classified from their paths and headers.
|
||||
- `HOPPER_LOADER_ARGS_JSON` (optional): JSON array overriding derived Hopper loader arguments for supported executable targets, e.g. `["-l","Mach-O","--aarch64"]`.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
|
||||
@@ -42,7 +45,7 @@ Use ESM TypeScript, two-space indentation, and Prettier defaults. Keep compiler
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
Name tests `*.test.ts`. Use Vitest and production seams (`tests/fixtures/`) rather than module mocks. Domain tests assert pure behavior; adapter tests use the fake launcher/socket; MCP tests connect with the beta.3 client. Preserve the 31 official-proxy plus 8 enhanced tool inventory (39 total). Cover malformed input, cancellation, timeouts, process exit, concurrency, limits, and clean shutdown. Real Hopper claims cannot be replaced by mocks — use `npm run verify:hopper` for end-to-end verification.
|
||||
Name tests `*.test.ts`. Use Vitest and production seams (`tests/fixtures/`) rather than module mocks. Domain tests assert pure behavior; adapter tests use the fake launcher/socket; MCP tests connect with the beta.3 client. Preserve the 31 direct, 8 enhanced, and 3 session tool inventory (42 total). Cover malformed input, cancellation, timeouts, process exit, concurrency, limits, and clean shutdown. Real Hopper claims cannot be replaced by mocks — use `npm run verify:hopper` with two distinct binaries for end-to-end verification.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
# REA
|
||||
|
||||
REA is agent-ready Hopper Disassembler tooling: one CLI with a beta.3 MCP mode, 31 official proxies, 8 enhanced reverse-engineering workflows, and 3 binary-session tools.
|
||||
REA gives coding agents access to Hopper Disassembler through one `rea` command. It exposes 42 MCP tools: 31 direct Hopper operations, 8 composed analysis workflows, and 3 target-session tools.
|
||||
|
||||
## Install from this checkout
|
||||
REA uses Hopper's public Python API through its own authenticated local bridge. It does not use Hopper's bundled MCP server.
|
||||
|
||||
Requires macOS 12+ and Node.js 22+.
|
||||
## Requirements
|
||||
|
||||
- macOS 12 or newer
|
||||
- Node.js 22 or newer
|
||||
- Hopper Disassembler
|
||||
|
||||
REA does not bundle Hopper. `rea setup --yes` can install Homebrew when absent, install Hopper with the `hopper-disassembler` Homebrew cask, configure detected MCP clients, and install the REA agent skill. Setup is idempotent and returns structured remediation when an installer needs human attention.
|
||||
|
||||
## Install
|
||||
|
||||
After publication:
|
||||
|
||||
```bash
|
||||
npx -y @morluto/rea setup --yes
|
||||
npx -y @morluto/rea doctor
|
||||
```
|
||||
|
||||
For source-checkout development:
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
@@ -13,17 +30,12 @@ npm link
|
||||
rea setup --yes
|
||||
```
|
||||
|
||||
`setup` is safe to repeat. It checks the host, installs Homebrew when approved and absent, installs Hopper from the official `hopper-disassembler` cask, and finishes with structured diagnostics. It never bundles Hopper or publishes this private package.
|
||||
|
||||
The CLI and MCP adapter share the same binary-session core. Source-checkout development uses `npm link`; packaged verification uses a local tarball and does not require a published package.
|
||||
|
||||
Useful onboarding commands:
|
||||
The CLI and MCP adapter call the same binary-session runtime directly; neither invokes the other. One-shot `analyze` and `decompile` commands close their bridge session before returning. MCP mode keeps one session alive so an agent can switch targets.
|
||||
|
||||
```bash
|
||||
rea --help
|
||||
rea --llms
|
||||
rea doctor --target /path/to/binary
|
||||
rea skills add
|
||||
rea mcp add
|
||||
```
|
||||
|
||||
@@ -33,12 +45,12 @@ Start the MCP adapter with `rea mcp` (or `rea --mcp`) with no target. In the con
|
||||
|
||||
1. Call `open_binary` with any readable local binary or `.hop` path.
|
||||
2. Call `binary_overview`, then use decompilation, strings, symbols, and xrefs tools.
|
||||
3. Call `open_binary` again to switch targets; a failed switch preserves the current target.
|
||||
3. Call `open_binary` again to switch targets. If the new target fails, REA attempts to reopen the previous target.
|
||||
4. Call `close_binary` when finished. `binary_session` reports current state.
|
||||
|
||||
Relative target paths resolve against the MCP server's working directory. Mach-O/FAT, ELF, PE, and Hopper databases are detected automatically. FAT loading selects the host-compatible architecture.
|
||||
|
||||
`HOPPER_TARGET_PATH` remains supported for older configurations but is optional. `HOPPER_LAUNCHER_PATH`, `HOPPER_TARGET_KIND`, and `HOPPER_LOADER_ARGS_JSON` retain their existing meanings for legacy startup configuration.
|
||||
`HOPPER_TARGET_PATH` is optional and opens an initial target at server startup. `HOPPER_LAUNCHER_PATH` overrides the Hopper launcher. `HOPPER_TARGET_KIND` selects `executable` or `database` for an initial target. `HOPPER_LOADER_ARGS_JSON` overrides REA's derived Hopper loader arguments for supported executable formats.
|
||||
|
||||
Manual target-free MCP configuration:
|
||||
|
||||
@@ -55,6 +67,20 @@ Manual target-free MCP configuration:
|
||||
|
||||
The first `open_binary` can take time while Hopper analyzes the file. Analysis calls made without an open target return an actionable `NoBinaryOpenError`.
|
||||
|
||||
## Hopper application behavior
|
||||
|
||||
REA starts Hopper when needed; Hopper does not have to be running first. Hopper's launcher internally activates the application, so opening a target may bring Hopper to the foreground. REA asks macOS to start Hopper hidden and in the background when possible, but cannot guarantee that Hopper will remain behind the current application.
|
||||
|
||||
Explicit format and architecture loader arguments prevent Hopper's common FAT/ARM selection dialogs. Other Hopper or macOS dialogs can still require a person. REA reports timeouts and setup remediation through the CLI or MCP result instead of trying to answer UI prompts.
|
||||
|
||||
Closing a REA session shuts down its bridge and removes its private socket directory. It does not quit a Hopper application the user may be using.
|
||||
|
||||
## Local security boundary
|
||||
|
||||
Each bridge session uses a random capability token and a Unix socket restricted to the current user. This rejects unauthenticated connections and access from other local user accounts, but it does not defend against a malicious process already running as the same user. REA bounds protocol messages and exposes sanitized error messages instead of launcher stderr or internal exception causes.
|
||||
|
||||
This boundary prevents unrelated local processes from accidentally using a bridge session; it is not a sandbox. Opening an untrusted binary still delegates parsing and analysis to Hopper under the current macOS user account.
|
||||
|
||||
## Development and verification
|
||||
|
||||
```bash
|
||||
@@ -66,11 +92,11 @@ HOPPER_SECOND_TARGET_PATH=/path/to/distinct-target-b \
|
||||
npm run verify:hopper
|
||||
```
|
||||
|
||||
Real-Hopper verification requires Hopper and a representative target. The stdio server uses only the repository's authenticated Unix-socket bridge; Hopper's bundled MCP server is not used.
|
||||
Real-Hopper verification requires Hopper and two distinct binaries. It checks target-free startup, all 42 tools, analysis, bounded decompilation, target switching, bridge cleanup, and server shutdown. The verifier terminates only Hopper bundle processes that appeared during its run; it preserves Hopper processes that were already running.
|
||||
|
||||
The package, executable, MCP, skill, and config names are centralized in `src/identity.ts`. The package remains private and is not ready for publication.
|
||||
Before publishing a release, run `npm run check`, `npm run verify:package`, `npm pack --dry-run`, and the two-target real-Hopper verifier. After publication, repeat the CLI and MCP smoke tests through `npx -y @morluto/rea`.
|
||||
|
||||
The package deliberately ships only the `rea` executable. A future zero-install setup will use the floating registration `npx -y @morluto/rea mcp`; local tarballs exercise the same packaged command before publication:
|
||||
The package ships only the `rea` executable. Setup writes a floating MCP registration using `npx -y @morluto/rea mcp`. A local tarball exercises the same package boundary before publication:
|
||||
|
||||
```bash
|
||||
npm pack
|
||||
|
||||
+13
-1
@@ -1,4 +1,9 @@
|
||||
"""Repository-owned Hopper adapter. Executed inside Hopper by its documented -Y flag."""
|
||||
"""Authenticated REA adapter executed on Hopper's dedicated Python thread.
|
||||
|
||||
The bootstrap injects ``REA_SOCKET`` and a random ``REA_TOKEN`` before executing
|
||||
this file with Hopper's supported ``--python`` launcher option. Keep all Hopper
|
||||
API access on this thread: moving dispatch to a worker can deadlock Hopper.
|
||||
"""
|
||||
|
||||
import json
|
||||
import hmac
|
||||
@@ -16,6 +21,7 @@ def _hex(value):
|
||||
|
||||
|
||||
def _json_safe(value):
|
||||
"""Project Hopper-specific Python values into the JSON protocol boundary."""
|
||||
if value is None or isinstance(value, (bool, int, float, str)):
|
||||
return value
|
||||
if isinstance(value, (list, tuple)):
|
||||
@@ -26,6 +32,7 @@ def _json_safe(value):
|
||||
|
||||
|
||||
def _document(name=None):
|
||||
"""Resolve an explicit or session-selected document without changing Hopper UI."""
|
||||
global _selected_document
|
||||
documents = Document.getAllDocuments()
|
||||
if name is not None:
|
||||
@@ -44,6 +51,7 @@ def _document(name=None):
|
||||
|
||||
|
||||
def _address(document, value=None):
|
||||
"""Resolve hexadecimal addresses first, then fall back to Hopper symbol names."""
|
||||
if value is None:
|
||||
return document.getCurrentAddress()
|
||||
if not isinstance(value, str):
|
||||
@@ -95,6 +103,7 @@ def _strings(document):
|
||||
|
||||
|
||||
def _assembly(procedure):
|
||||
"""Render bounded assembly while guarding against malformed instruction cycles."""
|
||||
lines = []
|
||||
segment = procedure.getSegment()
|
||||
seen = set()
|
||||
@@ -117,6 +126,7 @@ def _assembly(procedure):
|
||||
|
||||
|
||||
def _dispatch(method, params):
|
||||
"""Dispatch only the closed operation set implemented by REA's public tools."""
|
||||
global _selected_document
|
||||
if method == "health":
|
||||
return {"name": "REA Hopper bridge", "version": "1.0.0"}
|
||||
@@ -227,6 +237,7 @@ def _dispatch(method, params):
|
||||
|
||||
|
||||
def _serve_connection(connection):
|
||||
"""Serve one size-bounded, capability-authenticated NDJSON connection."""
|
||||
file = connection.makefile("rwb")
|
||||
while True:
|
||||
line = file.readline(MAX_LINE_BYTES + 1)
|
||||
@@ -255,6 +266,7 @@ def _serve_connection(connection):
|
||||
|
||||
|
||||
def _run():
|
||||
"""Own a permission-restricted, single-client Unix socket for this bridge."""
|
||||
if os.path.exists(REA_SOCKET):
|
||||
os.unlink(REA_SOCKET)
|
||||
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Route production MCP before importing Incur. Incur owns registration helpers
|
||||
// such as `mcp add`, while only dist/main.js may serve the 42-tool stdio server.
|
||||
const args = process.argv.slice(2);
|
||||
const isMcpMode =
|
||||
args[0] === "--mcp" || (args.length === 1 && args[0] === "mcp");
|
||||
|
||||
@@ -13,10 +13,17 @@ import type { HopperToolPort } from "./HopperToolPort.js";
|
||||
export interface BinaryClient extends HopperToolPort {
|
||||
close(): Promise<void>;
|
||||
}
|
||||
/** Production seam for creating a target-scoped Hopper client. */
|
||||
/** Production seam for creating one unstarted client for a parsed target. */
|
||||
export type BinaryClientFactory = (target: BinaryTarget) => BinaryClient;
|
||||
|
||||
/** Serializes target transitions while allowing calls within the active session. */
|
||||
/**
|
||||
* Owns the single active target shared by CLI and MCP adapters.
|
||||
*
|
||||
* Target transitions are serialized because each client dispatches Hopper API
|
||||
* work on its dedicated Python thread, and switching targets tears that bridge
|
||||
* down. A failed switch recreates the previous target instead of retaining a
|
||||
* client whose bridge was already shut down.
|
||||
*/
|
||||
export class BinarySession implements HopperToolPort {
|
||||
#active:
|
||||
| { readonly target: BinaryTarget; readonly client: BinaryClient }
|
||||
@@ -25,7 +32,10 @@ export class BinarySession implements HopperToolPort {
|
||||
readonly #calls = new Set<Promise<unknown>>();
|
||||
constructor(readonly createClient: BinaryClientFactory) {}
|
||||
|
||||
/** Open or switch to a target, retaining the old session when the new one fails. */
|
||||
/**
|
||||
* Open or switch targets after draining calls against the current target.
|
||||
* Returns the switch failure even if best-effort restoration also fails.
|
||||
*/
|
||||
open(
|
||||
path: string,
|
||||
options: { readonly signal?: AbortSignal } = {},
|
||||
@@ -82,7 +92,11 @@ export class BinarySession implements HopperToolPort {
|
||||
};
|
||||
}
|
||||
|
||||
/** Invoke a Hopper tool against the active target. */
|
||||
/**
|
||||
* Invoke a Hopper tool against the active target.
|
||||
* Calls may overlap, but a pending target transition prevents new calls from
|
||||
* entering until the transition has settled.
|
||||
*/
|
||||
async callTool(
|
||||
name: string,
|
||||
arguments_: Readonly<Record<string, JsonValue>>,
|
||||
|
||||
@@ -2,7 +2,11 @@ import { parseConfig } from "../config.js";
|
||||
import type { JsonValue } from "../hopper/protocol.js";
|
||||
import { createBinarySession } from "./runtime.js";
|
||||
|
||||
/** Open one binary, execute one tool, and always release the target session. */
|
||||
/**
|
||||
* Open one binary, execute one tool, and always release the bridge session.
|
||||
* Unlike MCP mode, every CLI invocation is intentionally isolated and does not
|
||||
* retain a target or bridge resources for a subsequent command.
|
||||
*/
|
||||
export const runDirectAnalysis = async (
|
||||
path: string,
|
||||
tool: "binary_overview" | "procedure_pseudo_code",
|
||||
|
||||
@@ -19,7 +19,7 @@ export interface DoctorCheck {
|
||||
readonly detail?: string;
|
||||
readonly remediation?: string;
|
||||
}
|
||||
/** Host facts and effects required by diagnostics. */
|
||||
/** Read-only host capabilities required by diagnostics. */
|
||||
export interface DoctorHost {
|
||||
readonly platform: NodeJS.Platform;
|
||||
readonly nodeVersion: string;
|
||||
@@ -30,7 +30,11 @@ export interface DoctorHost {
|
||||
manualHopperPaths(): Promise<readonly string[]>;
|
||||
}
|
||||
|
||||
/** Check requirements and an optional target without mutating the host. */
|
||||
/**
|
||||
* Check requirements and an optional target without mutating the host.
|
||||
* Every failed check includes remediation suitable for either the one-shot CLI
|
||||
* or an agent consuming the structured result.
|
||||
*/
|
||||
export const runDoctor = async (
|
||||
target?: string,
|
||||
host: DoctorHost = systemDoctorHost(),
|
||||
|
||||
@@ -21,11 +21,15 @@ import type { JsonValue } from "../hopper/protocol.js";
|
||||
|
||||
type EnhancedResult = Promise<Result<JsonValue, HopperError>>;
|
||||
|
||||
/** Application services composing official Hopper operations into eight RE tools. */
|
||||
/**
|
||||
* Composes official Hopper operations into bounded reverse-engineering tools.
|
||||
* Inputs are parsed again at this application boundary even though MCP normally
|
||||
* validates them, allowing direct callers to use the same service safely.
|
||||
*/
|
||||
export class EnhancedTools {
|
||||
constructor(private readonly hopper: HopperToolPort) {}
|
||||
|
||||
/** Parse and dispatch one enhanced tool call. */
|
||||
/** Parse inputs again at the direct-call boundary, then dispatch exhaustively. */
|
||||
execute(
|
||||
name: EnhancedToolName,
|
||||
input: unknown,
|
||||
|
||||
@@ -4,6 +4,11 @@ import type { JsonValue } from "../hopper/protocol.js";
|
||||
|
||||
/** Application-owned capability for invoking one official Hopper tool. */
|
||||
export interface HopperToolPort {
|
||||
/**
|
||||
* Invoke a declared bridge operation with JSON-safe arguments.
|
||||
* Implementations return expected Hopper, timeout, and cancellation failures
|
||||
* as values and must honor an already-aborted signal.
|
||||
*/
|
||||
callTool(
|
||||
name: string,
|
||||
arguments_: Readonly<Record<string, JsonValue>>,
|
||||
|
||||
@@ -34,7 +34,11 @@ export type ClientConfigurationResult =
|
||||
readonly status: "failed";
|
||||
readonly reason: "backup" | "write" | "readback";
|
||||
};
|
||||
/** Effects required by the idempotent setup workflow. */
|
||||
/**
|
||||
* Effects required by the idempotent setup workflow.
|
||||
* Implementations report interrupted installers as values: setup must not wait
|
||||
* indefinitely for stdin or a GUI response in an unattended agent invocation.
|
||||
*/
|
||||
export interface SetupHost {
|
||||
readonly platform: NodeJS.Platform;
|
||||
readonly nodeVersion: string;
|
||||
@@ -48,7 +52,11 @@ export interface SetupHost {
|
||||
installSkill(): Promise<"installed" | "unchanged" | "failed">;
|
||||
doctor(): Promise<Awaited<ReturnType<typeof runDoctor>>>;
|
||||
}
|
||||
/** Structured setup outcome with actionable partial progress. */
|
||||
/**
|
||||
* Structured setup outcome intended to travel back through an agent to a human.
|
||||
* `needs_human` carries remediation text instead of initiating an interactive
|
||||
* prompt, avoiding deadlocks in stdio and other unattended environments.
|
||||
*/
|
||||
export interface SetupResult {
|
||||
readonly status: "ready" | "needs_human";
|
||||
readonly actions: readonly string[];
|
||||
@@ -57,7 +65,11 @@ export interface SetupResult {
|
||||
readonly remediation?: string;
|
||||
}
|
||||
|
||||
/** Install prerequisites and configure every detected supported client idempotently. */
|
||||
/**
|
||||
* Install prerequisites and configure detected clients idempotently.
|
||||
* With `yes` false, mutations requiring consent are reported as `needs_human`;
|
||||
* with `yes` true, external installers still fail closed if they require UI.
|
||||
*/
|
||||
export const runSetup = async (
|
||||
yes: boolean,
|
||||
host: SetupHost = systemSetupHost(),
|
||||
|
||||
@@ -5,7 +5,11 @@ export const HOMEBREW_COMMANDS = [
|
||||
"/usr/local/bin/brew",
|
||||
] as const;
|
||||
|
||||
/** Return the first successful result from the standard Homebrew executables. */
|
||||
/**
|
||||
* Return the first defined result from standard Homebrew executables.
|
||||
* A probe returning `undefined` means that location was unavailable or failed;
|
||||
* other falsy values are successful results and stop discovery.
|
||||
*/
|
||||
export const probeHomebrew = async <T>(
|
||||
probe: (command: string) => Promise<T | undefined>,
|
||||
): Promise<T | undefined> => {
|
||||
|
||||
@@ -5,7 +5,11 @@ import { BinarySession } from "./BinarySession.js";
|
||||
import { HopperApplicationLauncher } from "../hopper/BridgeLauncher.js";
|
||||
import { HopperClient } from "../hopper/HopperClient.js";
|
||||
|
||||
/** Create the target-switching session used by both the CLI and MCP adapters. */
|
||||
/**
|
||||
* Compose the target-switching runtime shared directly by CLI and MCP adapters.
|
||||
* This is the sole production wiring point for Hopper so neither adapter shells
|
||||
* out to the other and both retain identical loader and bridge semantics.
|
||||
*/
|
||||
export const createBinarySession = (config: AppConfig): BinarySession => {
|
||||
const bridgeScriptPath = fileURLToPath(
|
||||
new URL("../../bridge/hopper_bridge.py", import.meta.url),
|
||||
|
||||
+5
-1
@@ -6,7 +6,11 @@ import { runDirectAnalysis } from "./application/DirectAnalysis.js";
|
||||
import { runSetup } from "./application/Setup.js";
|
||||
import { PRODUCT_IDENTITY } from "./identity.js";
|
||||
|
||||
/** Build the Incur command tree for the non-MCP CLI commands. */
|
||||
/**
|
||||
* Build the one-shot Incur CLI without starting Hopper at import time.
|
||||
* Analysis commands acquire and close their own sessions; bare `mcp` and
|
||||
* `--mcp` are intercepted by the executable dispatcher before this module loads.
|
||||
*/
|
||||
export const createCli = (): ReturnType<typeof Cli.create> => {
|
||||
const cli = Cli.create(PRODUCT_IDENTITY.cliBinary, {
|
||||
version: "0.1.0",
|
||||
|
||||
+6
-1
@@ -20,7 +20,12 @@ const environmentSchema = z.object({
|
||||
HOPPER_LOADER_ARGS_JSON: z.string().optional(),
|
||||
});
|
||||
|
||||
/** Parse Hopper launcher configuration once at the composition root. */
|
||||
/**
|
||||
* Parse Hopper launcher configuration once at the composition root.
|
||||
* Explicit loader arguments override REA's header-derived defaults for a
|
||||
* supported target, allowing callers to refine Hopper's loader behavior without
|
||||
* bypassing REA's path and executable-header checks.
|
||||
*/
|
||||
export const parseConfig = (
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
): Result<AppConfig, ConfigurationError> => {
|
||||
|
||||
@@ -12,10 +12,10 @@ const caseSensitive = z
|
||||
.default(false)
|
||||
.describe("Whether to match case");
|
||||
|
||||
/** The architectural owner of a tool implementation. */
|
||||
/** Adapter family responsible for implementing a public MCP tool. */
|
||||
export type ToolKind = "official-proxy" | "enhanced" | "session";
|
||||
|
||||
/** Stable caller-visible contract retained across the Python-to-TypeScript migration. */
|
||||
/** Single source of truth for a public tool's name, description, and input. */
|
||||
export interface ToolContract {
|
||||
readonly name: string;
|
||||
readonly description: string;
|
||||
@@ -41,7 +41,7 @@ const session = (
|
||||
inputSchema: z.ZodObject,
|
||||
): ToolContract => ({ name, description, kind: "session", inputSchema });
|
||||
|
||||
/** All 31 tools forwarded unchanged to Hopper's official MCP server. */
|
||||
/** Bridge operations exposed without additional application composition. */
|
||||
export const OFFICIAL_TOOL_CONTRACTS = [
|
||||
official(
|
||||
"address_name",
|
||||
@@ -180,7 +180,7 @@ export const OFFICIAL_TOOL_CONTRACTS = [
|
||||
),
|
||||
] as const satisfies readonly ToolContract[];
|
||||
|
||||
/** All 8 reverse-engineering tools implemented by REA. */
|
||||
/** Bounded workflows composed from one or more bridge operations. */
|
||||
export const ENHANCED_TOOL_CONTRACTS = [
|
||||
enhanced(
|
||||
"swift_classes",
|
||||
@@ -224,7 +224,7 @@ export const ENHANCED_TOOL_CONTRACTS = [
|
||||
),
|
||||
] as const satisfies readonly ToolContract[];
|
||||
|
||||
/** Binary lifecycle tools available before Hopper is launched. */
|
||||
/** Target lifecycle tools available only on the long-lived MCP adapter. */
|
||||
export const SESSION_TOOL_CONTRACTS = [
|
||||
session(
|
||||
"open_binary",
|
||||
@@ -235,7 +235,11 @@ export const SESSION_TOOL_CONTRACTS = [
|
||||
session("binary_session", "Describe the active binary session", z.object({})),
|
||||
] as const satisfies readonly ToolContract[];
|
||||
|
||||
/** Complete caller-visible tool inventory. */
|
||||
/**
|
||||
* Complete ordered public inventory used by registration and verification.
|
||||
* Keep this collection at 42 tools unless a deliberate contract change updates
|
||||
* snapshots, package verification, and real-Hopper verification together.
|
||||
*/
|
||||
export const TOOL_CONTRACTS = [
|
||||
...OFFICIAL_TOOL_CONTRACTS,
|
||||
...ENHANCED_TOOL_CONTRACTS,
|
||||
|
||||
@@ -7,7 +7,11 @@ import { err, ok, type Result } from "./result.js";
|
||||
|
||||
/** CPU families understood by the supported executable loaders. */
|
||||
export type BinaryArchitecture = "x86" | "x86_64" | "arm" | "arm64";
|
||||
/** A parsed binary target and the Hopper launcher arguments it requires. */
|
||||
/**
|
||||
* A canonical local target plus deterministic Hopper loader arguments.
|
||||
* Explicit loader and architecture flags prevent Hopper from presenting modal
|
||||
* format or FAT-architecture selection dialogs during agent-driven analysis.
|
||||
*/
|
||||
export interface BinaryTarget {
|
||||
readonly path: string;
|
||||
readonly kind: "executable" | "database";
|
||||
@@ -17,7 +21,11 @@ export interface BinaryTarget {
|
||||
readonly loaderArgs: readonly string[];
|
||||
}
|
||||
|
||||
/** Resolve, read, and classify a local binary path, including its architecture metadata. */
|
||||
/**
|
||||
* Resolve and classify a readable local target before Hopper is launched.
|
||||
* FAT Mach-O inputs select only a host-compatible architecture so setup remains
|
||||
* non-interactive; unsupported or ambiguous inputs are returned as typed errors.
|
||||
*/
|
||||
export const parseBinaryTarget = async (
|
||||
input: string,
|
||||
cwd = process.cwd(),
|
||||
@@ -53,7 +61,11 @@ type ExecutableMetadata = Pick<
|
||||
"format" | "architecture" | "availableArchitectures" | "loaderArgs"
|
||||
>;
|
||||
|
||||
/** Parse supported executable headers without performing I/O. */
|
||||
/**
|
||||
* Parse supported executable headers without I/O and derive explicit Hopper
|
||||
* loader arguments. The caller must supply the host architecture used for FAT
|
||||
* Mach-O slice selection.
|
||||
*/
|
||||
export const parseExecutableHeader = (
|
||||
bytes: Buffer,
|
||||
hostArchitecture: NodeJS.Architecture,
|
||||
|
||||
@@ -38,7 +38,19 @@ export interface HopperApplicationLauncherOptions {
|
||||
readonly bridgeScriptPath: string;
|
||||
}
|
||||
|
||||
/** Launches Hopper through its documented CLI and injects only the owned bridge script. */
|
||||
/**
|
||||
* Launches Hopper through its documented CLI and injects only REA's owned bridge.
|
||||
*
|
||||
* Hopper's `hopper` helper internally issues an AppleScript `activate` command.
|
||||
* Consequently, opening a target may bring Hopper to the foreground even though
|
||||
* REA first asks macOS to start the application hidden and in the background.
|
||||
* REA cannot reliably suppress that activation without replacing Hopper's
|
||||
* supported launcher; callers must treat possible foreground UI as an upstream
|
||||
* Hopper constraint, not as evidence that analysis failed.
|
||||
*
|
||||
* The returned process is the short-lived launcher helper, not the Hopper app,
|
||||
* so REA does not claim ownership of the application's lifetime.
|
||||
*/
|
||||
export class HopperApplicationLauncher implements BridgeLauncher {
|
||||
constructor(readonly options: HopperApplicationLauncherOptions) {}
|
||||
|
||||
@@ -103,6 +115,8 @@ const prepareHopperApplication = async (
|
||||
if (appBundle === undefined) return;
|
||||
const executablePath = join(appBundle, "Contents/MacOS/Hopper Disassembler");
|
||||
if (await processIsRunning(executablePath)) return;
|
||||
// This reduces activation when Hopper is cold, but the vendor launcher that
|
||||
// follows may still activate its window. See HopperApplicationLauncher.
|
||||
await execFileAsync("/usr/bin/open", [
|
||||
"--hide",
|
||||
"--background",
|
||||
|
||||
+5
-1
@@ -1,4 +1,8 @@
|
||||
/** Public REA identifiers kept together for consistent packaging and setup. */
|
||||
/**
|
||||
* Canonical public identity shared by packaging, setup, MCP, and skill install.
|
||||
* Centralization prevents persistent client registrations from drifting from
|
||||
* the npm package or executable exposed in package.json.
|
||||
*/
|
||||
export const PRODUCT_IDENTITY = {
|
||||
displayName: "REA",
|
||||
packageName: "@morluto/rea",
|
||||
|
||||
+5
-1
@@ -12,7 +12,11 @@ import { parseConfig } from "./config.js";
|
||||
import { createBinarySession } from "./application/runtime.js";
|
||||
import { createServer } from "./server/createServer.js";
|
||||
|
||||
/** Start the production stdio server and install owned shutdown handlers. */
|
||||
/**
|
||||
* Start the long-lived MCP adapter and install idempotent shutdown handlers.
|
||||
* The adapter owns its BinarySession for the process lifetime; EOF and process
|
||||
* signals close bridge resources without assuming ownership of the Hopper app.
|
||||
*/
|
||||
export const run = async (): Promise<number> => {
|
||||
const config = parseConfig(process.env);
|
||||
if (!config.ok) {
|
||||
|
||||
@@ -7,7 +7,11 @@ import { registerEnhancedTools } from "./registerEnhancedTools.js";
|
||||
import { registerOfficialTools } from "./registerOfficialTools.js";
|
||||
import { registerSessionTools } from "./registerSessionTools.js";
|
||||
|
||||
/** Construct one MCP server instance without acquiring subprocess resources. */
|
||||
/**
|
||||
* Construct one MCP server without acquiring subprocess resources.
|
||||
* Supplying a session adds target lifecycle tools; omitting it retains the
|
||||
* fixed-target seam used by focused tests and embedders.
|
||||
*/
|
||||
export const createServer = (
|
||||
hopper: HopperToolPort,
|
||||
session?: BinarySession,
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "../contracts/toolContracts.js";
|
||||
import { toCallToolResult } from "./toolResult.js";
|
||||
|
||||
/** Register all eight enhanced reverse-engineering tools. */
|
||||
/** Register composed workflows against the same port as direct bridge tools. */
|
||||
export const registerEnhancedTools = (
|
||||
server: McpServer,
|
||||
hopper: HopperToolPort,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { jsonValueSchema, type JsonValue } from "../hopper/protocol.js";
|
||||
import { toCallToolResult } from "./toolResult.js";
|
||||
|
||||
/** Register every official Hopper proxy contract on one MCP server. */
|
||||
/** Register direct bridge proxies, preserving MCP cancellation and typed errors. */
|
||||
export const registerOfficialTools = (
|
||||
server: McpServer,
|
||||
hopper: HopperToolPort,
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { BinarySession } from "../application/BinarySession.js";
|
||||
import { SESSION_TOOL_CONTRACTS } from "../contracts/toolContracts.js";
|
||||
import { toCallToolResult } from "./toolResult.js";
|
||||
|
||||
/** Register binary lifecycle management tools. */
|
||||
/** Register MCP-only target lifecycle operations on a long-lived session. */
|
||||
export const registerSessionTools = (
|
||||
server: McpServer,
|
||||
session: BinarySession,
|
||||
|
||||
@@ -4,7 +4,11 @@ import type { HopperError } from "../domain/errors.js";
|
||||
import type { Result } from "../domain/result.js";
|
||||
import type { JsonValue } from "../hopper/protocol.js";
|
||||
|
||||
/** Translate an application result into caller-visible MCP content. */
|
||||
/**
|
||||
* Translate an application result into MCP text content.
|
||||
* Error tags and safe messages remain visible while underlying causes, process
|
||||
* output, and other potentially sensitive details stay private.
|
||||
*/
|
||||
export const toCallToolResult = (
|
||||
result: Result<JsonValue, HopperError>,
|
||||
): CallToolResult =>
|
||||
|
||||
Reference in new issue
Block a user