331 lines
17 KiB
Markdown
331 lines
17 KiB
Markdown
# hyperhive
|
|
|
|
Multi-Claude-Code-agent orchestration on **nixos-containers**. A host-side Rust
|
|
daemon (`hive-c0re`) spawns nspawn-isolated agent containers and brokers
|
|
messages between them. A manager agent (`hm1nd`) coordinates the swarm and
|
|
gates lifecycle changes on user approval via git commits, surfaced through a
|
|
vibec0re-styled HTTP dashboard with live SSE message-flow.
|
|
|
|
**PLAN.md** is the living design doc. Read it for the *why* and the phase
|
|
roadmap; this file is the operator/developer reference for the *how*.
|
|
|
|
## Architecture
|
|
|
|
```
|
|
host (NixOS, hive-c0re.service)
|
|
│
|
|
├── hive-c0re (Rust daemon — coordinator + dashboard + CLI)
|
|
│ ├── lifecycle — nixos-container CRUD (spawn/kill/rebuild/list)
|
|
│ ├── broker — sqlite message store + broadcast channel
|
|
│ ├── approvals — sqlite approval queue
|
|
│ ├── coordinator — shared state (broker/approvals/agent sockets)
|
|
│ ├── actions — approve/deny (shared between admin socket & dashboard)
|
|
│ ├── server — host admin socket (JSON line protocol)
|
|
│ ├── manager_server — manager-only privileged socket
|
|
│ ├── agent_server — per-sub-agent sockets
|
|
│ ├── dashboard — axum HTTP UI + SSE message-flow + approve/deny + T4LK
|
|
│ └── client — admin-socket client (powers `hive-c0re spawn|kill|…`)
|
|
│
|
|
├── /run/hyperhive/
|
|
│ ├── host.sock — admin CLI ↔ daemon
|
|
│ ├── manager.sock → hm1nd container at /run/hive/mcp.sock
|
|
│ └── agents/<name>/mcp.sock → h-<name> container at /run/hive/mcp.sock
|
|
│
|
|
├── /var/lib/hyperhive/
|
|
│ ├── broker.sqlite — messages + approvals tables
|
|
│ ├── agents/<name>/config/ — proposed repo (manager-editable, RO to hive-c0re)
|
|
│ └── applied/<name>/ — applied repo (hive-c0re-only, container builds here)
|
|
│
|
|
└── nixos-containers
|
|
├── h-<name> (sub-agents, hive-ag3nt binary)
|
|
└── hm1nd (manager, hive-m1nd binary)
|
|
```
|
|
|
|
## Crates / file map
|
|
|
|
```
|
|
hive-c0re/ host daemon + CLI (one binary, subcommand-dispatched)
|
|
src/main.rs clap setup; serve / spawn / kill / rebuild / list /
|
|
pending / approve / deny
|
|
src/server.rs host admin socket (HostRequest → dispatch)
|
|
src/client.rs admin-socket client
|
|
src/manager_server.rs manager-privileged socket (ManagerRequest)
|
|
src/agent_server.rs per-sub-agent socket listener
|
|
src/broker.rs sqlite Message store + broadcast channel for SSE
|
|
src/approvals.rs sqlite Approval queue
|
|
src/coordinator.rs shared state (broker/approvals/agent_flake/sockets)
|
|
src/actions.rs approve/deny (admin socket + dashboard both call in)
|
|
src/lifecycle.rs `nixos-container` shellouts, per-agent flake generator,
|
|
systemd drop-ins, git helpers, agent_web_port hash
|
|
src/dashboard.rs axum HTTP UI: containers list, T4LK form, approvals
|
|
(diff + Approve/Deny buttons), SSE message flow
|
|
|
|
hive-ag3nt/ in-container harness crate; produces TWO binaries
|
|
src/lib.rs DEFAULT_SOCKET, DEFAULT_WEB_PORT, re-exports
|
|
src/client.rs generic JSON-line request/response over unix socket
|
|
src/web_ui.rs per-container axum HTTP page (label + placeholder)
|
|
src/bin/hive-ag3nt.rs sub-agent CLI (serve/send/recv); turn loop + web UI
|
|
src/bin/hive-m1nd.rs manager CLI (serve/send/recv/spawn/kill/
|
|
request-apply-commit); recognises HelperEvent
|
|
|
|
hive-sh4re/ wire types (HostRequest/Response, AgentRequest/Response,
|
|
ManagerRequest/Response, Message, Approval, HelperEvent)
|
|
|
|
nix/
|
|
modules/hive-c0re.nix systemd service + firewall + git path wiring
|
|
templates/agent-base.nix sub-agent nixos-container template
|
|
templates/manager.nix manager nixos-container template
|
|
|
|
tests/roundtrip.sh Phase 3 messaging round-trip
|
|
tests/approval.sh Phase 5 end-to-end approval flow
|
|
tests/dashboard.sh Phase 6+7 HTTP dashboard + SSE + orphan GC
|
|
|
|
docs/damocles-migration.md options for moving damocles onto hyperhive
|
|
```
|
|
|
|
## Conventions
|
|
|
|
- **Naming.** Containers are length-bounded (`nixos-container` ≤ 11 chars).
|
|
Sub-agents are `h-<name>` with `<name>` ≤ 9 chars; the manager is `hm1nd`.
|
|
`MAX_AGENT_NAME` enforces the cap in `lifecycle.rs`. Per-agent web UI port =
|
|
`WEB_PORT_BASE + FNV1a(name) % WEB_PORT_RANGE` (8100..8999); manager fixed
|
|
at 8000; dashboard `cfg.dashboardPort` (default 7000).
|
|
- **Identity = socket.** No auth/tokens on the per-agent sockets. The socket
|
|
*path* identifies the principal; perms come from "who has the bind-mount."
|
|
- **Wire protocol.** JSON line-delimited over unix sockets in both directions
|
|
(host admin / manager / agent). `/messages/stream` is `text/event-stream`.
|
|
- **Commit messages.** Short, lowercase, no Co-Authored-By trailer.
|
|
- **Commit before test.** Stage and commit when work *looks* ready, then run
|
|
validation (`cargo check`, `nix flake check`, real lpt2 deploy). Failures get
|
|
a follow-up commit rather than an amend.
|
|
- **`rebuild` is the reconcile verb.** Idempotently rewrites
|
|
`/etc/nixos-containers/<C>.conf` (`PRIVATE_NETWORK=0`, clears
|
|
HOST_ADDRESS/LOCAL_ADDRESS, sets `EXTRA_NSPAWN_FLAGS`), regenerates
|
|
`applied/<name>/flake.nix`, writes the systemd limits drop-in, then
|
|
`nixos-container update` + stop + start. Anything that changes per-container
|
|
state on the host should be re-applied here.
|
|
- **Actions are factored.** `approve` / `deny` live in `actions.rs`; the admin
|
|
socket and the dashboard POST handlers both call into them, so the two
|
|
surfaces never drift.
|
|
|
|
## Gotchas / lessons learned
|
|
|
|
- **`nixos-container` doesn't expose `--bind` on the CLI.** Path is via
|
|
`EXTRA_NSPAWN_FLAGS` in `/etc/nixos-containers/<NAME>.conf` — the start
|
|
script (`/nix/store/.../container_-start`) expands it unquoted into the
|
|
`systemd-nspawn` invocation. We rewrite this line in `set_nspawn_flags()`.
|
|
- **`/run/systemd/nspawn/*.nspawn` overrides are *ignored*** by
|
|
`nixos-container`'s start script (it builds the nspawn cmd line directly).
|
|
- **`boot.isNspawnContainer = true`**, not `boot.isContainer = true`. Renamed
|
|
in nixos-25.11+.
|
|
- **`nixos-container create` auto-assigns `HOST_ADDRESS`/`LOCAL_ADDRESS`** in
|
|
the `.conf`. The start script's `if HOST_ADDRESS set → --network-veth`
|
|
branch then forces a private netns — which is silently fatal for our web
|
|
UIs (the bind is invisible from the host). We force-clear those vars (and
|
|
`HOST_ADDRESS6` / `LOCAL_ADDRESS6` / `HOST_BRIDGE`) plus set
|
|
`PRIVATE_NETWORK=0`.
|
|
- **systemd service PATH ≠ host PATH.** Our service explicitly sets
|
|
`path = [ pkgs.git "/run/current-system/sw" ]`. Additionally,
|
|
`environment.HYPERHIVE_GIT = "${pkgs.git}/bin/git"` bakes the absolute path
|
|
in (read by `lifecycle::git_command()`) so git resolution doesn't depend on
|
|
PATH plumbing at all.
|
|
- **`RuntimeDirectoryPreserve = "yes"`** keeps `/run/hyperhive/` (and the
|
|
per-agent sub-dirs) across `hive-c0re` restarts. Without it, every restart
|
|
wipes bind sources and existing containers can't be started.
|
|
- **`register_agent` is idempotent** — drops any prior socket task before
|
|
rebinding. Required so a `hive-c0re` restart followed by `rebuild alice`
|
|
recreates the agent's socket without needing a clean reinstall.
|
|
- **`claude-code` is unfree.** `agent-base.nix` allow-list's it specifically.
|
|
The flake pins it to **nixpkgs-unstable** via `overlays.claude-unstable`
|
|
(stable lags too far). The overlay imports unstable with its own
|
|
`allowUnfreePredicate` so the access inside the overlay doesn't itself trip.
|
|
- **Claude credentials are stateful and per-container.** No `ANTHROPIC_API_KEY`
|
|
env var path. Today's stopgap: `nixos-container root-login h-<name>` →
|
|
`claude` (interactive) → log in once. The harness falls back to echo
|
|
replies when `claude --print` fails. **Phase 8** moves this to a per-agent
|
|
persistent dir at `/var/lib/hyperhive/agents/<name>/claude/` bind-mounted
|
|
into the container, with the interactive login driven from the agent's web
|
|
UI. Sharing one `~/.claude` across agents is NOT viable — OAuth refresh
|
|
tokens rotate, so any sibling refresh invalidates all the others.
|
|
- **Echo guard.** `hive-ag3nt serve` skips auto-reply when the incoming body
|
|
starts with `"echo: "`. Prevents ping-pong loops when both sides fall back
|
|
to echo. Real conversations between claude-backed agents *will* runaway —
|
|
bounding them is the manager's job.
|
|
- **Orphan approvals.** If state dirs are wiped out from under a pending
|
|
approval (test scripts, manual `rm -rf`), the dashboard's next render
|
|
marks them `failed` with note `"agent state dir missing"` so they fall out
|
|
of `pending`. They stay in sqlite for audit.
|
|
|
|
## Build / deploy / test
|
|
|
|
```sh
|
|
# inside the repo (devshell first; no global cargo)
|
|
nix develop -c cargo check
|
|
nix develop -c cargo clippy --workspace --all-targets -- -D warnings
|
|
nix develop -c cargo build
|
|
|
|
# evaluate everything (incl. rust+nix+toml fmt + clippy)
|
|
nix flake check
|
|
|
|
# build only the workspace package
|
|
nix build .#default
|
|
./result/bin/{hive-c0re,hive-ag3nt,hive-m1nd}
|
|
|
|
# deploy to an existing host that imports hyperhive.nixosModules.hive-c0re
|
|
cd ~/Repos/<nixos-config-repo>
|
|
nix flake update --update-input hyperhive
|
|
sudo nixos-rebuild switch --flake .#<host>
|
|
sudo systemctl restart hive-c0re # if only env/options changed
|
|
|
|
# end-to-end tests (each idempotent; runs as root)
|
|
sudo bash tests/roundtrip.sh # alice ↔ bob echo round-trip
|
|
sudo bash tests/approval.sh # manager edit → request → user approve → rebuilt
|
|
sudo bash tests/dashboard.sh # HTTP UI, approve POST, SSE, orphan GC
|
|
```
|
|
|
|
The host config also needs `hyperhive.overlays.default` applied — the module's
|
|
default `package = pkgs.hyperhive` requires the overlay to bring the package
|
|
in. The `claude-unstable` overlay is applied internally to per-agent flakes
|
|
already.
|
|
|
|
## Phase status
|
|
|
|
- ✅ Phase 0 — repo + Cargo workspace + flake + agent-base template
|
|
- ✅ Phase 1 — container lifecycle; `nixos-container update` hot-reload works
|
|
under the patch stack (validated on muede-lpt2)
|
|
- ✅ Phase 2 — per-agent sockets, in-memory broker, agent harness round-trips
|
|
- ✅ Phase 3 — sqlite broker (durable) + claude-or-echo turn loop
|
|
- ✅ Phase 4 — `hm1nd` manager binary + manager socket + declarative
|
|
`containers.hm1nd`
|
|
- ✅ Phase 5 — git-commit approval flow
|
|
- 5a — sqlite approval queue (`request_apply_commit`/`pending`/`approve`/`deny`)
|
|
- 5b — per-agent config flakes
|
|
- 5c — manager edits `proposed`, hive-c0re writes-only `applied`; container
|
|
builds from `applied`. Approve = read `agent.nix` at the approved commit
|
|
from `proposed`, copy into `applied`, commit + rebuild. Manager cannot
|
|
move `applied/main` on its own.
|
|
- ✅ Phase 6 — per-container web UIs (`HIVE_PORT` deterministic-hash) +
|
|
hive-c0re dashboard (default 7000, vibec0re aesthetic, deep-linked)
|
|
- ✅ Phase 7 — polish:
|
|
- 7a — dashboard Approve/Deny buttons + unified diff (`similar` crate)
|
|
- 7b — broker broadcast + `/messages/stream` SSE + live message-flow panel
|
|
- 7c — `ApprovalResolved` helper events into manager inbox
|
|
- 7d — `MemoryMax=2G` + `CPUQuota=50%` systemd drop-in per container
|
|
- 7e — damocles migration plan (`docs/damocles-migration.md`)
|
|
- ✅ Phase 7 follow-ups:
|
|
- Dashboard **T4LK** form — operator can send messages from the browser
|
|
(`POST /send`, becomes `from: "operator"` broker message)
|
|
- Orphan-approval GC on dashboard render (stale entries auto-failed)
|
|
- `PRIVATE_NETWORK=0` + `HOST_ADDRESS=`/`LOCAL_ADDRESS=` cleared in
|
|
`set_nspawn_flags` so sub-agent web UI ports are reachable on the host
|
|
- `HYPERHIVE_GIT` env var (absolute path) bypasses PATH ambiguity
|
|
|
|
## Phase 8 — real claude in containers + login UX (in progress)
|
|
|
|
See PLAN.md → "Phase 8" for the full design. Summary:
|
|
|
|
- **Per-agent persistent creds dir.** Bind
|
|
`/var/lib/hyperhive/agents/<name>/claude/` → `/root/.claude` (RW) in
|
|
`set_nspawn_flags`. One OAuth lineage per agent; refresh rotations stay
|
|
contained to that agent.
|
|
- **State dirs persist by default.** `destroy` keeps
|
|
`/var/lib/hyperhive/agents/<name>/` unless the operator passes an explicit
|
|
wipe flag. Recreating an agent of the same name reuses prior creds.
|
|
- **First spawn is approval-gated.** New agent names go through the same
|
|
approval queue as config edits. Manager calls `RequestSpawn` (CLI:
|
|
`hive-m1nd request-spawn <name>`); operator can also queue from the
|
|
dashboard or `hive-c0re request-spawn <name>`. The host's direct
|
|
`hive-c0re spawn <name>` still works as a privileged bypass for tests.
|
|
Approve runs `lifecycle::spawn` in a background task; the dashboard polls
|
|
via `<meta refresh>` and renders a spinner row while
|
|
`nixos-container create` + `update` + `start` is in flight.
|
|
- **"needs login" partial-run state.** No valid session in `~/.claude/` →
|
|
harness binds the web UI but does NOT start the turn loop. The harness
|
|
polls the dir; as soon as a login lands it transitions into the turn loop
|
|
without a restart. Dashboard surfaces the state per-agent via a `needs
|
|
login` badge in the container list. "Valid session" today is a heuristic
|
|
(any regular file inside `/root/.claude/`); we may refine once the
|
|
filename layout claude writes is locked in.
|
|
- **Login from the per-agent web UI.** Spawn `claude /login` with plain
|
|
stdio pipes (no PTY initially), surface the OAuth URL from stdout on the
|
|
page, accept the resulting code via a paste field, write it to the process
|
|
stdin. On success, harness transitions out of "needs login" and enters the
|
|
turn loop. If pipes turn out to be insufficient (claude refuses without a
|
|
TTY, raw-mode input, ANSI-only output) we redo the backend with a PTY.
|
|
|
|
Implementation order: bind-mount/dir creation → approval-gated spawn +
|
|
spinner → "needs login" partial run → PTY login endpoint. The login UI has
|
|
nowhere to live until the partial-run mode exists, so don't ship it earlier.
|
|
|
|
## Approval flow
|
|
|
|
End-to-end: manager edits per-agent `proposed` repo → commits → submits commit
|
|
sha → user approves on host CLI **or** dashboard button → `hive-c0re` reads the
|
|
file at that sha from `proposed`, applies into `applied`, commits there, runs
|
|
`nixos-container update`. Helper-event JSON lands in the manager's inbox.
|
|
|
|
```
|
|
# Inside the hm1nd container (manager has /agents bind-mounted RW):
|
|
cd /agents/alice/config
|
|
$EDITOR agent.nix # e.g. environment.systemPackages = [ pkgs.htop ];
|
|
git commit -am "add htop"
|
|
SHA=$(git rev-parse HEAD)
|
|
hive-m1nd request-apply-commit alice $SHA
|
|
exit
|
|
|
|
# On the host (CLI):
|
|
sudo hive-c0re pending # shows queued approval with id N
|
|
sudo hive-c0re approve N # validates, applies, rebuilds
|
|
sudo nixos-container run h-alice -- which htop
|
|
|
|
# Or on the dashboard (browser):
|
|
http://<host>:7000/ # ◆ APPR0VE button next to the diff
|
|
```
|
|
|
|
Per-agent layout — two separate git repos:
|
|
|
|
```
|
|
/var/lib/hyperhive/agents/<name>/config/ # proposed — manager edits, hive-c0re reads only
|
|
├── .git/
|
|
└── agent.nix # the only file the manager can change
|
|
# (initial commit by hive-c0re on first spawn,
|
|
# never touched by hive-c0re again)
|
|
|
|
/var/lib/hyperhive/applied/<name>/ # applied — hive-c0re-only; container builds here
|
|
├── .git/
|
|
├── flake.nix # hive-c0re-managed; references hyperhive_flake
|
|
└── agent.nix # overwritten by approve from the proposed commit
|
|
```
|
|
|
|
The container's `--flake` ref is `<applied_dir>#default`. The flake's
|
|
`nixosConfigurations.default` extends `hyperhive.nixosConfigurations.agent-base`
|
|
with `./agent.nix` plus an inline module that sets
|
|
`environment.etc."gitconfig".text` (committer identity = the agent's name) and
|
|
`systemd.services.hive-ag3nt.environment.HIVE_PORT`/`HIVE_LABEL`.
|
|
|
|
## Polish backlog
|
|
|
|
Not phased — pick when relevant:
|
|
|
|
- **Operator inbox view** — drain replies addressed to `operator` and show
|
|
in the dashboard (today they accumulate in sqlite unread).
|
|
- **Per-agent UI substance** — show last N inbox messages, last turn timing,
|
|
link back to dashboard.
|
|
- **xterm.js terminal** — embed in each per-container UI, attach to a PTY
|
|
exposed by the harness.
|
|
- **`destroy` verb** — currently `nixos-container destroy` + manual `rm -rf`.
|
|
Should be one hive-c0re verb that also purges approvals + state dirs.
|
|
- **Bounded broker** — cap rows per recipient or auto-vacuum delivered
|
|
messages older than a threshold.
|
|
- **Container crash events** — watch `container@*.service` via D-Bus,
|
|
push `HelperEvent::ContainerCrash` to the manager.
|
|
|
|
## Inspirations
|
|
|
|
- **`~/Repos/bitburner-agent`** — sibling project, drives Claude Code in a
|
|
turn loop against a Bitburner CDP session. Patterns to steal as we grow:
|
|
per-cycle prompt diffing (vs full state), notes compaction as a separate
|
|
short-lived Claude session, MCP server registering tools from a single
|
|
`TOOLS` array, dashboard with SSE + xterm.js + sqlite stats sampler,
|
|
opaque "terminal event" stream that unifies tool-call / sleep / op-notice
|
|
/ etc.
|