diff --git a/README.md b/README.md index 9052bdb8..5db00466 100644 --- a/README.md +++ b/README.md @@ -8,83 +8,26 @@ coordinates the swarm and gates lifecycle changes on user approval via git commits, surfaced through a vibec0re-styled HTTP dashboard. ``` -host (NixOS, runs hive-c0re.service) -│ -├── operator -│ ├── browser → :7000 hive-c0re dashboard (containers, approvals) -│ ├── browser → :8000 / :8100-8999 per-agent web UIs (live SSE, send, login) -│ └── CLI → /run/hyperhive/host.sock JSON-line admin protocol -│ -├── hive-c0re (Rust daemon) -│ ├── lifecycle nixos-container CRUD + per-agent flake generation -│ ├── broker sqlite messages + tokio broadcast (powers SSE + wake-ups) -│ ├── approvals sqlite queue, two kinds: ApplyCommit (config) + Spawn -│ ├── auto_update rebuilds any container whose recorded flake rev is stale -│ ├── dashboard axum HTTP + async-form actions + SSE message flow -│ └── sockets /run/hyperhive/{host,manager,agents/}/mcp.sock -│ -└── nixos-containers (each bind-mounts its socket dir → /run/hive, - │ its credentials dir → /root/.claude; - │ manager additionally gets /agents RW) - │ - ├── hm1nd hive-m1nd serve : claude turn loop + - │ MCP (send / recv / request_spawn / kill / - │ request_apply_commit) + web UI on :8000 - │ - └── h- hive-ag3nt serve : claude turn loop + - MCP (send / recv) + web UI on a hashed :8100-8999 + ┌────────────────────────┐ + │ hive-c0re (Rust) │ + operator ──▶ │ • lifecycle │ ─▶ nixos-containers + │ • broker (sqlite) │ ├── hm1nd (manager) + │ • approvals (sqlite) │ ├── h-alice (sub-agent) + │ • dashboard :7000 │ └── h-bob ... + │ • per-agent sockets │ + └────────────────────────┘ ``` -Each turn: harness pops one inbox message (Recv long-polls server-side and -wakes on a broker Sent event) → builds a wake prompt → spawns -`claude --print --continue --output-format stream-json --mcp-config …` → -streams JSON events into the per-agent SSE bus → claude drives any further -`recv`/`send` itself via the embedded MCP server. +Each container runs a harness binary that drives `claude --print --continue` +in a turn loop, exposes a per-agent web UI with a live event stream, and +talks to the broker over a bind-mounted unix socket via an embedded MCP +server claude calls into. -Config changes flow the other way: manager edits `/agents//config/agent.nix` -(bind-mounted from the host's proposed repo) → commits → submits the sha as -an approval → operator clicks ◆ APPR0VE on the dashboard → hive-c0re copies -the file into the applied repo and `nixos-container update`s the agent. +## Read next -## Host config - -Minimal `flake.nix` for a host that runs hive-c0re: - -```nix -{ - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; - hyperhive.url = "git+https://git.berlin.ccc.de/vinzenz/hyperhive"; - }; - - outputs = { nixpkgs, hyperhive, ... }: { - nixosConfigurations.my-host = nixpkgs.lib.nixosSystem { - system = "x86_64-linux"; - modules = [ - hyperhive.nixosModules.hive-c0re - ({ ... }: { - nixpkgs.overlays = [ - hyperhive.overlays.default - hyperhive.overlays.claude-unstable - ]; - services.hive-c0re = { - enable = true; - hyperhiveFlake = "${hyperhive}"; - }; - # ... rest of your host config (hardware, networking, users, …) - system.stateVersion = "25.11"; - }) - ]; - }; - }; -} -``` - -hive-c0re will then: -- open its admin socket at `/run/hyperhive/host.sock` + dashboard on - `:7000`, -- auto-create the manager container (`hm1nd`) if missing, -- auto-rebuild any managed container whose hyperhive rev is stale. +- **[CLAUDE.md](CLAUDE.md)** — developer reference: conventions, gotchas, + per-subsystem design, file map. +- **[TODO.md](TODO.md)** — open work items. ## Build / deploy diff --git a/tests/approval.sh b/tests/approval.sh new file mode 100755 index 00000000..2210d5ab --- /dev/null +++ b/tests/approval.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# Phase 5c end-to-end approval flow: +# manager edits proposed -> commits -> request_apply_commit +# user approves on host -> hive-c0re applies into authoritative repo -> rebuild +# sub-agent container has the new package +# +# Runs as root on a host with services.hive-c0re enabled and the hm1nd +# container declared. Idempotent — wipes any prior alice state. + +set -euo pipefail + +AGENT=alice +PKG=htop + +cleanup() { + echo "=== cleanup ===" + sudo hive-c0re kill "$AGENT" 2>/dev/null || true + sudo nixos-container destroy "h-${AGENT}" 2>/dev/null || true + sudo rm -rf \ + "/var/lib/hyperhive/agents/${AGENT}" \ + "/var/lib/hyperhive/applied/${AGENT}" +} + +echo "=== precheck: hm1nd container is up ===" +if ! sudo machinectl status hm1nd >/dev/null 2>&1; then + echo " hm1nd is not running. Starting via systemd..." + sudo systemctl start "container@hm1nd.service" || { + echo "FAIL: could not start container@hm1nd.service." + echo " Did you 'nixos-rebuild switch' after declaring containers.hm1nd in your host config?" + exit 1 + } + # wait briefly for it to come up + for i in 1 2 3 4 5 6 7 8 9 10; do + sudo machinectl status hm1nd >/dev/null 2>&1 && break + sleep 0.5 + done +fi +echo " hm1nd is up ✓" + +cleanup + +echo "=== spawn ${AGENT} ===" +sudo hive-c0re spawn "$AGENT" + +echo "=== two-repo split visible ===" +echo " proposed (manager-editable):" +sudo ls -la "/var/lib/hyperhive/agents/${AGENT}/config/" | sed 's/^/ /' +echo " applied (hive-c0re only):" +sudo ls -la "/var/lib/hyperhive/applied/${AGENT}/" | sed 's/^/ /' + +echo "=== manager cannot see the applied repo ===" +if sudo nixos-container run hm1nd -- ls "/var/lib/hyperhive/applied/${AGENT}" 2>/dev/null; then + echo "FAIL: manager can see applied/ — bind-mount leak" + exit 1 +fi +echo " manager has no path to applied/ ✓" + +echo "=== ${PKG} not installed in h-${AGENT} (pre-approve) ===" +if sudo nixos-container run "h-${AGENT}" -- which "$PKG" 2>/dev/null; then + echo "FAIL: ${PKG} already in path" + exit 1 +fi +echo " not in path ✓" + +echo "=== manager: edit agent.nix + commit + request_apply_commit ===" +sudo nixos-container run hm1nd -- bash -c " + set -euo pipefail + cd /agents/${AGENT}/config + cat > agent.nix <<'EOF' +{ pkgs, ... }: +{ + environment.systemPackages = [ pkgs.${PKG} ]; +} +EOF + git commit -am 'add ${PKG}' + SHA=\$(git rev-parse HEAD) + echo \" manager commit SHA=\$SHA\" + hive-m1nd request-apply-commit ${AGENT} \$SHA +" + +echo "=== pending approvals ===" +sudo hive-c0re pending + +ID=$(sudo hive-c0re pending \ + | sed -nE 's/^[[:space:]]*"id":[[:space:]]*([0-9]+).*/\1/p' \ + | tail -1) +if [ -z "$ID" ]; then + echo "FAIL: could not parse approval id" + exit 1 +fi +echo " using approval id ${ID}" + +echo "=== approve ${ID} (advances applied/main + rebuilds h-${AGENT}) ===" +sudo hive-c0re approve "$ID" + +echo "=== verify ${PKG} now in path ===" +sudo nixos-container run "h-${AGENT}" -- which "$PKG" + +echo "=== applied repo git log ===" +sudo git -C "/var/lib/hyperhive/applied/${AGENT}" log --oneline -5 + +echo "=== approvals table ===" +if command -v sqlite3 >/dev/null; then + sudo sqlite3 /var/lib/hyperhive/broker.sqlite \ + "SELECT id, agent, substr(commit_ref,1,12) AS sha, status FROM approvals ORDER BY id DESC LIMIT 5;" +else + echo " (sqlite3 not on host PATH — skip)" +fi + +echo +read -r -p "press enter to tear down, Ctrl-C to leave running: " +cleanup diff --git a/tests/dashboard.sh b/tests/dashboard.sh new file mode 100755 index 00000000..c2683750 --- /dev/null +++ b/tests/dashboard.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env bash +# Phase 6 + 7 smoke test: dashboard HTTP, per-container web UIs, +# approve-by-POST, SSE endpoint, and orphan-approval GC. +# +# Runs as root on a host with services.hive-c0re enabled and the hm1nd +# container declared. Idempotent — wipes any prior alice state. + +set -euo pipefail + +AGENT=alice +PKG=htop +HOST=${HOST:-localhost} +DASH_PORT=${DASH_PORT:-7000} +MGR_PORT=${MGR_PORT:-8000} + +cleanup() { + sudo hive-c0re kill "$AGENT" 2>/dev/null || true + sudo nixos-container destroy "h-${AGENT}" 2>/dev/null || true + sudo rm -rf \ + "/var/lib/hyperhive/agents/${AGENT}" \ + "/var/lib/hyperhive/applied/${AGENT}" +} + +echo "=== precheck: hm1nd container is up ===" +if ! sudo machinectl status hm1nd >/dev/null 2>&1; then + echo " starting via systemd..." + sudo systemctl start container@hm1nd.service + for i in 1 2 3 4 5 6 7 8 9 10; do + sudo machinectl status hm1nd >/dev/null 2>&1 && break + sleep 0.5 + done +fi +echo " ✓ hm1nd is up" + +cleanup + +echo "=== dashboard reachable ===" +curl -sf "http://${HOST}:${DASH_PORT}/" | grep -q "HYPERHIVE" || { + echo "FAIL: dashboard not serving expected content" + exit 1 +} +echo " ✓ http://${HOST}:${DASH_PORT}/ → HTML with HYPERHIVE banner" + +echo "=== /messages/stream SSE endpoint ===" +curl -sI "http://${HOST}:${DASH_PORT}/messages/stream" \ + | grep -qi "content-type: text/event-stream" || { + echo "FAIL: /messages/stream is not SSE" + exit 1 +} +echo " ✓ Content-Type: text/event-stream" + +echo "=== manager UI reachable ===" +curl -sf "http://${HOST}:${MGR_PORT}/" | grep -q "hm1nd" || { + echo "FAIL: manager UI not reachable at :${MGR_PORT}" + exit 1 +} +echo " ✓ http://${HOST}:${MGR_PORT}/ → hm1nd label" + +echo "=== spawn ${AGENT} ===" +sudo hive-c0re spawn "$AGENT" + +echo "=== pick up ${AGENT}'s port from dashboard ===" +sleep 2 +ALICE_PORT=$(curl -sf "http://${HOST}:${DASH_PORT}/" \ + | sed -nE "s|.*href=\"http://[^:]+:([0-9]+)/\">${AGENT}<.*|\\1|p" \ + | head -1) +if [ -z "$ALICE_PORT" ]; then + echo "FAIL: ${AGENT} port not found in dashboard" + exit 1 +fi +echo " ✓ ${AGENT} port: ${ALICE_PORT}" + +echo "=== ${AGENT} UI reachable ===" +for i in 1 2 3 4 5 6 7 8 9 10; do + curl -sf "http://${HOST}:${ALICE_PORT}/" | grep -q "${AGENT}" && break + sleep 1 +done +curl -sf "http://${HOST}:${ALICE_PORT}/" | grep -q "${AGENT}" || { + echo "FAIL: ${AGENT} UI not reachable at :${ALICE_PORT}" + exit 1 +} +echo " ✓ http://${HOST}:${ALICE_PORT}/ → ${AGENT} label" + +echo "=== ${PKG} not in path pre-approve ===" +if sudo nixos-container run "h-${AGENT}" -- which "$PKG" 2>/dev/null; then + echo "FAIL: ${PKG} already in path" + exit 1 +fi +echo " ✓" + +echo "=== manager submits approval (via hm1nd) ===" +sudo nixos-container run hm1nd -- bash -c " + set -euo pipefail + cd /agents/${AGENT}/config + cat > agent.nix <<'EOF' +{ pkgs, ... }: +{ + environment.systemPackages = [ pkgs.${PKG} ]; +} +EOF + git commit -am 'add ${PKG}' + SHA=\$(git rev-parse HEAD) + echo \" manager commit SHA=\$SHA\" + hive-m1nd request-apply-commit ${AGENT} \$SHA +" + +echo "=== pick approval id from dashboard ===" +ID=$(curl -sf "http://${HOST}:${DASH_PORT}/" \ + | sed -nE 's|.*hive-c0re approve ([0-9]+).*|\1|p' \ + | head -1) +if [ -z "$ID" ]; then + echo "FAIL: approval id not found in dashboard" + exit 1 +fi +echo " ✓ approval id: ${ID}" + +echo "=== POST /approve/${ID} (browser button equivalent) ===" +HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' \ + -X POST "http://${HOST}:${DASH_PORT}/approve/${ID}") +if [ "$HTTP_CODE" != "303" ] && [ "$HTTP_CODE" != "302" ]; then + echo "FAIL: expected 303 redirect, got HTTP ${HTTP_CODE}" + exit 1 +fi +echo " ✓ HTTP ${HTTP_CODE} (redirect)" + +echo "=== verify ${PKG} now in h-${AGENT} ===" +sudo nixos-container run "h-${AGENT}" -- which "$PKG" + +echo "=== orphan-GC: cleanup ${AGENT}, refresh dashboard, orphan disappears ===" +sudo nixos-container run hm1nd -- bash -c " + set -euo pipefail + cd /agents/${AGENT}/config + echo '{ ... }: { }' > agent.nix + git commit -am 'noop' + SHA=\$(git rev-parse HEAD) + hive-m1nd request-apply-commit ${AGENT} \$SHA +" +ORPHAN_ID=$(sudo hive-c0re pending \ + | sed -nE 's/^[[:space:]]*"id":[[:space:]]*([0-9]+).*/\1/p' \ + | tail -1) +echo " queued orphan id: ${ORPHAN_ID}" +cleanup +# First dashboard render triggers GC. +curl -sf "http://${HOST}:${DASH_PORT}/" >/dev/null +sleep 0.5 +if sudo hive-c0re pending | grep -q "\"id\": ${ORPHAN_ID}"; then + echo "FAIL: orphan ${ORPHAN_ID} still pending after dashboard render" + exit 1 +fi +echo " ✓ orphan ${ORPHAN_ID} cleaned up" + +echo +echo "=== summary ===" +echo " dashboard http://${HOST}:${DASH_PORT}/" +echo " manager UI http://${HOST}:${MGR_PORT}/" +echo " (alice torn down)" diff --git a/tests/roundtrip.sh b/tests/roundtrip.sh new file mode 100755 index 00000000..8f152070 --- /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