From 4f191b2e4361ef228de652cdaf45b24b32febea1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?m=C3=BCde?= Date: Thu, 14 May 2026 22:29:25 +0200 Subject: [PATCH] tests: roundtrip smoke; CLAUDE.md --- CLAUDE.md | 157 +++++++++++++++++++++++++++++++++++++++++++++ tests/roundtrip.sh | 61 ++++++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 CLAUDE.md create mode 100755 tests/roundtrip.sh diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1f2f80a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,157 @@ +# hyperhive + +Multi-Claude-Code-agent orchestration on **nixos-containers**. A host-side Rust +daemon spawns nspawn-isolated agent containers and brokers messages between +them. Eventually a manager agent (another Claude Code session in its own +container) coordinates the swarm and gates lifecycle changes on user approval +via git commits. + +**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 +├── hive-c0re (Rust daemon, NixOS service) +│ ├── lifecycle — nixos-container CRUD +│ ├── broker — sqlite message store (/var/lib/hyperhive/broker.sqlite) +│ ├── server — host admin socket (JSON line protocol) +│ └── agent_server — per-agent MCP-ish sockets +│ +├── /run/hyperhive/ +│ ├── host.sock — admin CLI ↔ daemon +│ └── agents//mcp.sock — bind-mounted into each container at /run/hive +│ +└── nixos-containers + ├── h- (sub-agents, hive-ag3nt binary) + └── hm1nd (manager, hive-m1nd binary — Phase 4+) +``` + +## Crates / file map + +``` +hive-c0re/ host daemon + CLI (one binary, subcommand-dispatched) + main.rs clap setup; serve vs spawn/kill/rebuild/list + server.rs host admin socket + client.rs host admin socket client (for spawn/kill/rebuild/list) + broker.rs sqlite-backed Message store (rusqlite) + agent_server.rs per-agent socket listener + coordinator.rs shared runtime state (broker + map) + lifecycle.rs `nixos-container` shellouts (spawn/kill/rebuild/list) + +hive-ag3nt/ in-container harness; produces TWO binaries from one crate + src/lib.rs DEFAULT_SOCKET, re-exports + src/client.rs AgentRequest/AgentResponse over /run/hive/mcp.sock + src/bin/hive-ag3nt.rs sub-agent CLI (serve/send/recv) + src/bin/hive-m1nd.rs manager placeholder (Phase 4) + +hive-sh4re/ wire types (HostRequest/Response, AgentRequest/Response, Message) + +nix/ + modules/hive-c0re.nix systemd service wiring + templates/agent-base.nix nixos-container template (boot.isNspawnContainer = true) + +tests/roundtrip.sh Phase 3 end-to-end smoke test +``` + +## Conventions + +- **Naming.** Containers are length-bounded (`nixos-container` ≤ 11 chars). + Sub-agents are `h-` with `` ≤ 9 chars; the manager is `hm1nd`. + `MAX_AGENT_NAME` enforces the cap in `lifecycle.rs`. +- **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. + See `hive-sh4re` for the types. (Phase 6+ may swap to real MCP stdio.) +- **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.** It rewrites `/etc/nixos-containers/.conf` + EXTRA_NSPAWN_FLAGS idempotently *and* does `nixos-container update` *and* + stop+start so nspawn-level changes (bind mounts) take effect. Anything that + changes per-container state on the host should be re-applied here. + +## Gotchas / lessons learned + +- **`nixos-container` doesn't expose `--bind` on the CLI.** Path is via + `EXTRA_NSPAWN_FLAGS` in `/etc/nixos-containers/.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). Don't bother. +- **`boot.isNspawnContainer = true`**, not `boot.isContainer = true`. The + latter was renamed in nixos-25.11+. +- **systemd service PATH ≠ host PATH.** Our service explicitly sets + `path = [ "/run/current-system/sw" ]` so `nixos-container` (which lives in + the system profile, not nixpkgs) is reachable. +- **`RuntimeDirectoryPreserve = "yes"`** keeps `/run/hyperhive/` (and the + 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. For now: `nixos-container root-login h-` → `claude` + (interactive) → log in once. The harness falls back to echo replies when + `claude --print` fails. Future: bind-mount a shared `~/.claude` dir from the + host so creds survive container destroy/recreate. +- **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 — that's + the manager's job to bound (Phase 4+). + +## Build / deploy / test + +```sh +# inside the repo (devshell first; no global cargo) +nix develop -c cargo check +nix develop -c cargo build + +# evaluate everything (incl. fmt check) +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/ +nix flake update --update-input hyperhive +sudo nixos-rebuild switch --flake .# + +# end-to-end test (lpt2 or any host with the module enabled) +sudo bash tests/roundtrip.sh +``` + +The host config also needs `hyperhive.overlays.default` applied — the module's +default `package = pkgs.hyperhive` requires the overlay to bring the package +in. + +## Phase status + +- ✅ Phase 0 — repo + Cargo workspace + flake + agent-base template +- ✅ Phase 1 — container lifecycle (spawn/kill/rebuild/list); nixos-container update + hot-reload works under the patch stack (validated empirically on muede-lpt2) +- ✅ Phase 2 — per-agent sockets, in-memory broker, agent harness round-trips messages +- ✅ Phase 3 — sqlite broker (durable across restart) + claude-or-echo turn loop +- 🔜 Phase 4 — `hm1nd` manager binary with privileged tool surface +- 🔜 Phase 5 — git-commit approval flow (`state-repo` + per-agent config flakes) +- 🔜 Phase 6 — per-agent web UI + dashboard MVP +- 🔜 Phase 7 — dashboard commit-view + polish + +See PLAN.md for the full design and the deferred-out-of-scope list. + +## 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. diff --git a/tests/roundtrip.sh b/tests/roundtrip.sh new file mode 100755 index 0000000..8f15207 --- /dev/null +++ b/tests/roundtrip.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# Phase 3 end-to-end smoke test: spawn alice + bob, send alice→bob, observe +# bob's harness echo-reply land in alice's journal. +# +# Run as root (uses nixos-container + hive-c0re). Idempotent — wipes any prior +# h-alice / h-bob first. +# +# With claude not logged in inside the containers, both fall back to echo and +# the chain terminates after one round trip (the "echo: " guard). + +set -euo pipefail + +cleanup() { + echo "=== cleaning up any prior test agents ===" + sudo hive-c0re kill alice 2>/dev/null || true + sudo hive-c0re kill bob 2>/dev/null || true + sudo nixos-container destroy h-alice 2>/dev/null || true + sudo nixos-container destroy h-bob 2>/dev/null || true +} + +cleanup + +echo "=== spawn alice + bob ===" +sudo hive-c0re spawn alice +sudo hive-c0re spawn bob + +echo "=== wait for harnesses to come up ===" +sleep 3 + +T=$(date +"%Y-%m-%d %H:%M:%S") +echo "test start: $T" + +echo "=== alice → bob: 'ping' ===" +sudo nixos-container run h-alice -- hive-ag3nt send bob "ping" + +echo "=== wait for bob to reply, alice to receive ===" +sleep 4 + +echo +echo "=== bob's journal since test start ===" +sudo journalctl -M h-bob -u hive-ag3nt --since "$T" --no-pager | tail -20 || true + +echo +echo "=== alice's journal since test start ===" +sudo journalctl -M h-alice -u hive-ag3nt --since "$T" --no-pager | tail -20 || true + +echo +echo "=== broker rows (last 10) ===" +sudo sqlite3 /var/lib/hyperhive/broker.sqlite \ + "SELECT id, sender, recipient, substr(body,1,60) AS body, sent_at, delivered_at FROM messages ORDER BY id DESC LIMIT 10;" \ + 2>/dev/null || echo "(sqlite3 not available — skip)" + +echo +echo "=== expected ===" +echo " bob's journal: 'inbox from=alice body=ping' then 'claude failed; falling back to echo'" +echo " alice's journal: 'inbox from=bob body=echo: ping' (no reply, echo guard stopped the chain)" +echo " broker rows: 2 messages — alice→bob 'ping' (delivered) and bob→alice 'echo: ping' (delivered)" + +echo +read -r -p "Press enter to tear down test agents, Ctrl-C to leave them up: " +cleanup