diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 1f2f80af..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,157 +0,0 @@ -# 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/flake.lock b/flake.lock index 06e16854..b97b56a2 100644 --- a/flake.lock +++ b/flake.lock @@ -59,27 +59,10 @@ "type": "github" } }, - "nixpkgs-unstable": { - "locked": { - "lastModified": 1778672786, - "narHash": "sha256-Blg88K1jwG+P0Mr27+rKMFCufdrWkV3wWh9AdYtz0FQ=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "eef00dfd8a712b34af845f9350bac681b1228bd1", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, "root": { "inputs": { "naersk": "naersk", "nixpkgs": "nixpkgs", - "nixpkgs-unstable": "nixpkgs-unstable", "treefmt-nix": "treefmt-nix" } }, diff --git a/hive-c0re/src/broker.rs b/hive-c0re/src/broker.rs index a88bbd8e..0eb25552 100644 --- a/hive-c0re/src/broker.rs +++ b/hive-c0re/src/broker.rs @@ -31,8 +31,8 @@ impl Broker { std::fs::create_dir_all(parent) .with_context(|| format!("create db parent {}", parent.display()))?; } - let conn = - Connection::open(path).with_context(|| format!("open broker db {}", path.display()))?; + let conn = Connection::open(path) + .with_context(|| format!("open broker db {}", path.display()))?; conn.execute_batch(SCHEMA).context("apply broker schema")?; Ok(Self { conn: Mutex::new(conn), diff --git a/hive-c0re/src/coordinator.rs b/hive-c0re/src/coordinator.rs index b2ba2f90..20c510f9 100644 --- a/hive-c0re/src/coordinator.rs +++ b/hive-c0re/src/coordinator.rs @@ -34,7 +34,8 @@ impl Coordinator { std::fs::create_dir_all(&agent_dir) .with_context(|| format!("create agent dir {}", agent_dir.display()))?; let socket_path = Self::socket_path(name); - let socket = agent_server::start(name.to_owned(), socket_path, self.broker.clone()).await?; + let socket = + agent_server::start(name.to_owned(), socket_path, self.broker.clone()).await?; self.agents.lock().unwrap().insert(name.to_owned(), socket); Ok(agent_dir) } diff --git a/nix/templates/agent-base.nix b/nix/templates/agent-base.nix index c633925a..4a56a340 100644 --- a/nix/templates/agent-base.nix +++ b/nix/templates/agent-base.nix @@ -2,7 +2,8 @@ { boot.isNspawnContainer = true; - nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (pkgs.lib.getName pkg) [ "claude-code" ]; + nixpkgs.config.allowUnfreePredicate = + pkg: builtins.elem (pkgs.lib.getName pkg) [ "claude-code" ]; environment.systemPackages = with pkgs; [ hyperhive diff --git a/tests/roundtrip.sh b/tests/roundtrip.sh deleted file mode 100755 index 8f152070..00000000 --- a/tests/roundtrip.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/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