Watch
0
0
Fork
You've already forked hyperhive
0

add rea agent module and reverse-engineer archetype

This commit is contained in:
atlas 2026-10-27 11:32:17 +01:00
commit 50c1f2a3d1
15 changed files with 2083 additions and 8 deletions

View file

@ -103,6 +103,9 @@ box it gets:
- **GUI** (opt-in, `services.hyperhive.agent.gui.enable`) — a Wayland
desktop with screenshot / keyboard / mouse tools, viewable over VNC in
the web UI.
- **reverse engineering** (opt-in, `services.hyperhive.agent.rea.enable`, or the
`reverse-engineer` archetype) — REA and Ghidra 12.1 in the container, with
their MCP tools allowed. → [archetypes](docs/swarm/archetypes.md)
- **your own tools** — any extra MCP server (stdio or http) via
`services.hyperhive.agent.extraMcpServers`, and any NixOS config you'd
put on a machine.

View file

@ -16,9 +16,10 @@ archetype. Picking "none" creates a custom agent, with its own config repo.
The hyperhive flake ships these:
| archetype | what it sets |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `reviewer` | permissions `messaging`, `inbox`, `execution`: the default set without `meta`. With `execution` it can run `hive-forge`, `git` and other commands. |
| `android-dev` | installs `android-tools` (`adb`, `fastboot`), Gradle and a JDK; default permissions. |
| `reverse-engineer` | sets `services.hyperhive.agent.rea.enable`: installs REA, Ghidra 12.1 and procps, and allows all REA MCP tools; default permissions and the agent's normal memory cap. |
## Where an archetype comes from

View file

@ -15,7 +15,8 @@ entry (`type = "stdio" | "http"`, default `"stdio"`): `matrix` and
built-in surface and `bash`'s `hive-bash-daemon`. The GUI bridge
(`screen`, gated on `services.hyperhive.agent.gui.enable`) is the one
entry on the schema's stdio default — it spawns
`hive-screen-mcp` fresh each turn. The server name is `hyperhive`, so
`hive-screen-mcp` fresh each turn. `rea` (gated on
`services.hyperhive.agent.rea.enable`) is a second stdio entry, spawning `rea mcp`. The server name is `hyperhive`, so
the tools land in claude as
`mcp__hyperhive__<tool>`. Each entry also has an `availableToSubagents`
toggle (default `false`) — see

View file

@ -101,6 +101,8 @@
frontend
reference-docs
claude-plugins
rea
ghidra-12-1
;
};
};

View file

@ -88,6 +88,7 @@ in
./otel-removed.nix
./packages.nix
./queue-identity.nix
./rea.nix
./user.nix
./screen.nix
./weston-vnc.nix

View file

@ -14,7 +14,8 @@
`hive-bash-daemon`, `hive-subagent-daemon`,
`hive-forge`, `hive-forge-notify`, `hive-github-notify`,
`hive-matrix-daemon`,
`hive-metric`, `hive-screen-mcp`) plus the `assets`, `frontend`,
`hive-metric`, `hive-screen-mcp`), the `rea` and `ghidra-12-1` packages
that `services.hyperhive.agent.rea` installs, plus the `assets`, `frontend`,
`reference-docs` and `claude-plugins` trees. Wired by the flake's agent-base
nixosModule to `hyperhive.packages.<system>.*`; override an
individual key per-agent to swap in a patched binary.

63
nix/agent-modules/rea.nix Normal file
View file

@ -0,0 +1,63 @@
# REA ("Reverse Engineer Anything") for the agent: `services.hyperhive.agent.rea.enable`
# puts the `rea` CLI, Ghidra 12.1 (with its JDK) and procps in the container and
# registers `rea mcp` as `extraMcpServers.rea` with every tool auto-approved.
#
# - Ghidra is the pinned nixpkgs' `ghidra-bin` recipe at 12.1.x (see
# ../packages/ghidra-12-1.nix); REA rejects the pinned 12.0.4. Both packages
# come in through `services.hyperhive.agent.packages` only when this is on, so an
# agent without it keeps the shared closure.
# - REA's headless Ghidra needs a procps-compatible `ps` on PATH.
# - The Ghidra project lives under the agent's state dir. REA needs the `.rep`
# directory next to the `.gpr` path to exist, so tmpfiles creates both as the
# agent user.
# - The JVM heap (`GHIDRA_HEADLESS_MAXMEM`) shares the agent's memory cap
# (`agentMemoryMax`); nothing here raises it.
{
pkgs,
lib,
config,
...
}:
let
cfg = config.services.hyperhive.agent;
userName = cfg.user.name;
projectDir = "/agents/${userName}/state/rea";
ghidra = cfg.packages.ghidra-12-1;
env = {
GHIDRA_INSTALL_DIR = "${ghidra}/lib/ghidra";
JAVA_HOME = "${pkgs.openjdk21}";
REA_GHIDRA_PROJECT_PATH = "${projectDir}/analysis.gpr";
GHIDRA_HEADLESS_MAXMEM = "2G";
};
in
{
options.services.hyperhive.agent.rea.enable = lib.mkEnableOption ''
REA (Reverse Engineer Anything) with Ghidra in the container. Installs `rea`,
Ghidra 12.1 and procps, sets `GHIDRA_INSTALL_DIR`, `JAVA_HOME`,
`REA_GHIDRA_PROJECT_PATH` (under the agent's state dir) and
`GHIDRA_HEADLESS_MAXMEM`, and registers `rea mcp` as
`extraMcpServers.rea` with all its tools allowed. Ghidra's 2G heap counts
against the agent's normal memory cap
'';
config = lib.mkIf cfg.rea.enable {
services.hyperhive.agent.extraMcpServers.rea = {
command = "${cfg.packages.rea}/bin/rea";
args = [ "mcp" ];
inherit env;
allowedTools = [ "*" ];
};
environment.systemPackages = [
cfg.packages.rea
ghidra
pkgs.procps
];
environment.variables = env;
systemd.tmpfiles.rules = [
"d ${projectDir} 0700 ${userName} ${userName} -"
"d ${projectDir}/analysis.rep 0700 ${userName} ${userName} -"
];
};
}

View file

@ -31,4 +31,12 @@
description = "Builds Android apps: adb and fastboot (android-tools), Gradle and a JDK are installed. It holds the default permissions.";
};
};
reverse-engineer = {
module = {
services.hyperhive.agent.rea.enable = true;
};
meta = {
description = "Reverse-engineers binaries and applications: REA and Ghidra 12.1 are installed with their MCP tools allowed. It holds the default permissions.";
};
};
}

View file

@ -218,6 +218,11 @@ in
inherit (pkgs) lib;
shared = moduleEvalShared;
};
module-eval-agent-rea = import ./module-eval/agent-rea.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
shared = moduleEvalShared;
};
# Clippy via crane's first-class `cargoClippy` builder. Reuses the
# shared `cargoArtifacts` (deps already built) and runs

View file

@ -0,0 +1,81 @@
# `checks.module-eval-agent-rea` — see ./lib.nix for the shared
# rationale (why this suite exists, naming convention, "evaluates
# not executes").
{
pkgs,
lib,
self,
nixosSystem,
shared,
}:
let
inherit
(import ./lib.nix {
inherit
pkgs
lib
self
nixosSystem
;
})
agentWith
runGroup
;
# The option is switched on by the archetype that exists for it, so this one
# fixture is the "on" arm and the archetype's own wiring.
agentRea = agentWith self.hyperhiveArchetypes.reverse-engineer.module;
agentNoRea = shared.agentBare;
rea = agentRea.services.hyperhive.agent.extraMcpServers.rea;
pkgsOf = machine: machine.environment.systemPackages;
reaTmpfiles = machine: lib.filter (lib.hasInfix "/state/rea") machine.systemd.tmpfiles.rules;
in
runGroup "agent-rea" [
{
# `rea` is the presence control for every absence below: the same lookups
# find the entry, the variables, the packages and the directories when
# the option is on.
name = "enabling rea registers its MCP server with every tool allowed and sets Ghidra's environment";
ok =
rea.allowedTools == [ "*" ]
&& rea.args == [ "mcp" ]
&& lib.hasSuffix "/bin/rea" rea.command
&& lib.all (n: rea.env ? ${n} && agentRea.environment.variables ? ${n}) [
"GHIDRA_INSTALL_DIR"
"JAVA_HOME"
"REA_GHIDRA_PROJECT_PATH"
"GHIDRA_HEADLESS_MAXMEM"
]
&& rea.env.GHIDRA_HEADLESS_MAXMEM == "2G"
&& lib.hasPrefix "/agents/a1/state/" rea.env.REA_GHIDRA_PROJECT_PATH;
}
{
name = "enabling rea installs rea and Ghidra 12.1, and creates the agent-owned project directories";
ok =
let
packages = agentRea.services.hyperhive.agent.packages;
in
lib.all (p: lib.elem p (pkgsOf agentRea)) [
packages.rea
packages.ghidra-12-1
]
&& lib.versionAtLeast packages.ghidra-12-1.version "12.1"
&& lib.length (reaTmpfiles agentRea) == 2
&& lib.all (lib.hasInfix " a1 a1 ") (reaTmpfiles agentRea);
}
{
name = "an agent without rea gets no REA server, no Ghidra variables, no REA packages and no project directories";
ok =
let
packages = agentNoRea.services.hyperhive.agent.packages;
in
!agentNoRea.services.hyperhive.agent.rea.enable
&& !(agentNoRea.services.hyperhive.agent.extraMcpServers ? rea)
&& !(agentNoRea.environment.variables ? GHIDRA_INSTALL_DIR)
&& !(agentNoRea.environment.variables ? REA_GHIDRA_PROJECT_PATH)
&& !(lib.elem packages.rea (pkgsOf agentNoRea))
&& !(lib.elem packages.ghidra-12-1 (pkgsOf agentNoRea))
&& reaTmpfiles agentNoRea == [ ];
}
]

View file

@ -149,7 +149,7 @@ let
# output and prints it as JSON. A derivation or a path in `meta` would
# make that evaluation build or copy something; a function would not
# print at all.
name = "the built-in archetypes carry a module and a meta of plain data, and reviewer can run commands and android-dev installs its toolchain";
name = "the built-in archetypes carry a module and a meta of plain data, and reviewer can run commands and android-dev installs its toolchain and reverse-engineer turns rea on";
ok =
let
plain =
@ -174,7 +174,8 @@ let
&& !(lib.elem "meta" reviewer)
&&
lib.elem pkgs.android-tools
(archetypes.android-dev.module { inherit pkgs; }).environment.systemPackages;
(archetypes.android-dev.module { inherit pkgs; }).environment.systemPackages
&& archetypes.reverse-engineer.module.services.hyperhive.agent.rea.enable;
}
{
# `docs.enable` reads `docs.source`, whose default builds from the

View file

@ -276,6 +276,10 @@ in
# is exposed too since it's independently useful/inspectable.
inherit swagger-ui-dist swagger-ui-theme;
# REA and the Ghidra it needs; consumed by nix/agent-modules/rea.nix.
rea = pkgs.callPackage ./rea.nix { };
ghidra-12-1 = pkgs.callPackage ./ghidra-12-1.nix { };
# Pre-built system closure every agent container builds from. Exposed as a package
# so operators can `nix build .#agent-base-toplevel` (or wire
# them into their host system closure via the

View file

@ -0,0 +1,14 @@
{ ghidra-bin, fetchzip }:
# Ghidra 12.1.4 on the pinned nixpkgs' `ghidra-bin` build recipe. REA accepts
# Ghidra >= 12.1 and the pinned nixpkgs ships 12.0.4. Only agents that enable
# `services.hyperhive.agent.rea` reference this, so the shared agent closure
# keeps the pinned Ghidra.
ghidra-bin.overrideAttrs (_: rec {
version = "12.1.4";
versiondate = "20260921";
src = fetchzip {
url = "https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${version}_build/ghidra_${version}_PUBLIC_${versiondate}.zip";
hash = "sha256-ZD4Vs1i7BMvrneAbAiOc7F9iBj64Rbom+PaVO//TzK8=";
};
})

1833
nix/packages/rea-package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

57
nix/packages/rea.nix Normal file
View file

@ -0,0 +1,57 @@
{
lib,
buildNpmPackage,
fetchurl,
jq,
nodejs_22,
}:
# REA ("Reverse Engineer Anything", npm `rea-agents`) from the published
# npm tarball, whose `dist/` is prebuilt: building from the git source runs a
# `compile` step that needs `tsrs`, a native compiler with no nix package.
#
# The tarball carries no lockfile, so ./rea-package-lock.json is vendored: a
# `npm install --package-lock-only` over the tarball's `dependencies` alone.
# Regenerate it on a version bump. `postPatch` strips the tarball's
# `package.json` to the same keys, so the install is production dependencies
# only; `frida`, an optional dependency whose install script downloads a
# toolchain, is left out with them, and `--ignore-scripts` keeps every other
# install script from running.
#
# Node must satisfy `^22.23.4 || ^24.22.1 || >=26.12.1`; nixpkgs' `nodejs_22`
# is the one that does.
buildNpmPackage (finalAttrs: {
pname = "rea-agents";
version = "6.8.0";
nodejs = nodejs_22;
src = fetchurl {
url = "https://registry.npmjs.org/rea-agents/-/rea-agents-${finalAttrs.version}.tgz";
hash = "sha256-jVQux2qcs1BYS2CmycCIcG/4EtW7WwlMIMlbbcJWmLs=";
};
sourceRoot = "package";
postPatch = ''
cp ${./rea-package-lock.json} package-lock.json
${lib.getExe jq} '{ name, version, license, bin, dependencies }' package.json > package.json.new
mv package.json.new package.json
'';
npmDepsHash = "sha256-/YMAZNQHaerVCVUlxTkL2WJyb9FXiu8ZpuBah3fW0+w=";
npmFlags = [ "--ignore-scripts" ];
dontNpmBuild = true;
doInstallCheck = true;
installCheckPhase = ''
runHook preInstallCheck
HOME=$(mktemp -d) $out/bin/rea mcp --help
runHook postInstallCheck
'';
meta = {
description = "Reverse Engineer Anything: an MCP server driving Ghidra and other reverse-engineering tools";
homepage = "https://github.com/morluto/rea";
license = lib.licenses.mit;
mainProgram = "rea";
};
})