diff --git a/CLAUDE.md b/CLAUDE.md index 2da3cccf..4405782d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -217,7 +217,7 @@ frontend/ npm workspaces (esbuild → static dist). Built + agent.css → dist/{*.html, static/*}. hive-ag3nt/ in-container harness crate; produces ONE `hive` - binary for all agents + binary picking role from `HIVE_ROLE` env src/lib.rs re-exports + DEFAULT_SOCKET, DEFAULT_WEB_PORT src/client.rs generic JSON-line request/response over unix socket src/web_ui.rs per-container axum HTTP page (incl /api/cancel, @@ -255,13 +255,12 @@ hive-ag3nt/ in-container harness crate; produces ONE `hive` src/login_session.rs drives `claude auth login` over stdio pipes src/prompt.rs system prompt renderer: filters the unified template through `` / - `` markers (always - renders the agent section); owns - `write_system_prompt` (re-exported from - turn.rs for callers). + `` markers per + `mcp::Flavor`; owns `write_system_prompt` + (re-exported from turn.rs for callers). src/bin/hive.rs unified harness main (Serve + Mcp + Wake - subcommands); privilege boundary enforced - server-side at the broker socket. Static UI assets + subcommands); role from `HIVE_ROLE` env, picks + agent or manager wire surface server-side. Static UI assets served by ServeDir from $HIVE_STATIC_DIR (= hyperhive.frontend.mergedDist — default agent dist + per-agent extraFiles, set per the @@ -329,12 +328,11 @@ nix/ `services.hyperhive.domain`; gateway serves `.well-known/matrix/{client,server}` for auto-discovery; federation on, e2ee deferred - templates/harness-base.nix shared harness for all containers; single - `hive-ag3nt` service unit; `hyperhive.model` - option (HIVE_DEFAULT_MODEL) - templates/agent-base.nix entry-point for sub-agent containers - templates/manager.nix entry-point for ruth; adds forge noise-reduction - defaults (keepSubscriptions=false etc.) + templates/harness-base.nix shared harness for all containers; `hyperhive.role` + (`"agent"` | `"manager"`) drives service unit + forge + defaults; `hyperhive.model` option (HIVE_DEFAULT_MODEL) + templates/agent-base.nix thin role-setter (`hyperhive.role = "agent"`) + templates/manager.nix thin role-setter (`hyperhive.role = "manager"`) templates/weston-vnc.nix optional `hyperhive.gui.enable` — weston + VNC backend systemd unit; writes /etc/hyperhive/gui.json (vnc_port + auth) for diff --git a/docs/agent-hierarchy.md b/docs/agent-hierarchy.md index 2e3ce4b7..087326f6 100644 --- a/docs/agent-hierarchy.md +++ b/docs/agent-hierarchy.md @@ -202,19 +202,46 @@ nspawn agent. Open questions, not yet wired: - Filesystem: share parent's `/state` RW, or a sub-dir? - Identity: distinct broker recipient name, or address the parent? -## Harness systemd unit shape +## Harness systemd unit shape (per-role) -One harness binary (`hive`), one `harness-base.nix` template, one -service unit (`systemd.services.hive-ag3nt`) for all agents. There -is no longer a separate manager service name or role distinction in -the harness — privilege differences live server-side in the broker -socket (which tool groups and manager-surface calls each agent -receives). +One harness binary (`hive`), one `harness-base.nix` template, two +systemd units depending on `hyperhive.role`: -`agent-base.nix` and `manager.nix` both import `harness-base.nix`. -`manager.nix` additionally sets forge defaults to suppress the -subscription/participation firehose so ruth's inbox stays focused -on direct mentions, reviews, and assignments. +- `agent-base.nix` (`role = "agent"`) → `systemd.services.hive-ag3nt` +- `manager.nix` (`role = "manager"`) → `systemd.services.hive-m1nd` + +The unit names diverge but the binary is the same. `HIVE_ROLE` env +var picks the surface at startup (agent vs manager); naming the +units after the historical per-role binaries keeps dashboard log +queries, ExecStartPre paths, and ancestor PR diffs working without a +rename cascade. + +### Manager-only defaults + +`harness-base.nix` flips these when `hyperhive.role == "manager"`, +via `lib.mkDefault` so any agent can invert if needed: + +- `hyperhive.forge.keepSubscriptions = false` +- `hyperhive.forge.skipNotifyReasons = [ "subscribed" "participating" ]` + +Skips the subscription / participation firehose so the manager's +inbox only carries direct mentions, reviews, and assignments. Sub- +agents keep the noisier defaults so they see anything aimed at the +repos they're working on. + +### Standalone-eval fallbacks + +`nixosConfigurations.manager` must build standalone (without the +meta-flake's per-agent flake.nix wrapper). For the manager unit +that means a hardcoded `HIVE_LABEL` env value: + +- `HIVE_LABEL = "ruth"` — logical agent name; matches what `meta.rs` + injects at deploy time. + +Real deploys never read these — `meta::render_flake` overrides them +via the generated wrapper. They exist so the manager +`nixosConfigurations` evaluates cleanly even outside the meta-flake +boundary. ### Environment variables set on the unit @@ -229,6 +256,8 @@ on direct mentions, reviews, and assignments. directly on the unit, **not** via `environment.variables`, because the latter only populates `/etc/profile` which systemd services don't inherit. +- `HIVE_ROLE = config.hyperhive.role` — picks the binary surface + (agent / manager) at startup. ### `PATH` setup (the wrapper-dir trick) @@ -249,7 +278,8 @@ bit set` regardless of `hyperhive.user.passwordlessSudo`. ### `serviceConfig` highlights -- `ExecStart = pkgs.hyperhive/bin/hive serve` — single binary. +- `ExecStart = pkgs.hyperhive/bin/hive serve` — single binary, + surface picked from `HIVE_ROLE`. - `Restart = on-failure`, `RestartSec = 2` — keeps the harness resilient across transient crashes without thundering retries. - `RuntimeDirectory = "hive-config"` → `/run/hive-config/` owned by diff --git a/docs/turn-loop.md b/docs/turn-loop.md index 9e5955ad..88ac9b82 100644 --- a/docs/turn-loop.md +++ b/docs/turn-loop.md @@ -5,7 +5,8 @@ claude has access to in return. ## The loop -Each agent harness (`hive serve` — one binary for all agents) runs: +Each agent harness (`hive serve`, role set via `$HIVE_ROLE` — always +`"agent"`, one binary) runs: 1. Long-poll `Recv` on its socket. The host-side broker (`broker.rs::recv_blocking_batch`) returns immediately if there's @@ -56,11 +57,15 @@ Each agent harness (`hive serve` — one binary for all agents) runs: ## Harness binary shape -One `hive` binary for all agents. The earlier split into +One `hive` binary serves both roles. The split into `hive-ag3nt` + `hive-m1nd` was collapsed because the privilege boundary lives server-side at the broker socket -(`/run/hive/mcp.sock`): `ManagerRequest` calls are refused by the -standard agent socket regardless of who sends them. +(`/run/hive/mcp.sock`): an agent-flavor socket refuses +`ManagerRequest` calls regardless of who sends them, so there's no +escalation risk in shipping the same code to both. `main()` reads +`$HIVE_ROLE` (set by `harness-base.nix` from `hyperhive.role`; +defaults to `"agent"` for standalone `nix run` invocations) and +dispatches. Three subcommands: diff --git a/hive-ag3nt/Cargo.toml b/hive-ag3nt/Cargo.toml index 655b5ab3..ed5de53b 100644 --- a/hive-ag3nt/Cargo.toml +++ b/hive-ag3nt/Cargo.toml @@ -28,8 +28,10 @@ tracing-subscriber.workspace = true tempfile = "3" [[bin]] -# Unified harness binary for all agents. Privilege boundary is -# enforced server-side at the socket (tool groups / manager surface). -# See `docs/turn-loop.md::Harness binary shape`. +# Unified harness binary: both `agent` and `manager` code paths live +# here; the binary picks its role at startup from `HIVE_ROLE` (set by +# `harness-base.nix` from `hyperhive.role`). The privilege boundary is +# enforced server-side at the socket, so shipping both surfaces in one +# binary is safe. See `docs/turn-loop.md::Harness binary shape`. name = "hive" path = "src/bin/hive.rs" diff --git a/hive-ag3nt/src/bin/hive.rs b/hive-ag3nt/src/bin/hive.rs index a565b6ce..b90494ca 100644 --- a/hive-ag3nt/src/bin/hive.rs +++ b/hive-ag3nt/src/bin/hive.rs @@ -198,6 +198,7 @@ trait Surface { struct AgentSurface; impl Surface for AgentSurface { + async fn ack_turn(socket: &Path) { match client::request::<_, AgentResponse>(socket, &AgentRequest::AckTurn).await { Ok(AgentResponse::Ok) => {} diff --git a/hive-ag3nt/src/prompt.rs b/hive-ag3nt/src/prompt.rs index a86d9054..8173fc7e 100644 --- a/hive-ag3nt/src/prompt.rs +++ b/hive-ag3nt/src/prompt.rs @@ -269,7 +269,13 @@ shared closer // Real template's first agent line — keeps the renderer // honest about the {label} / {operator_pronouns} pair the // harness already relied on. - let rendered = render(&PRODUCTION_TEMPLATE, "alice", "they/them", None, None); + let rendered = render( + &PRODUCTION_TEMPLATE, + "alice", + "they/them", + None, + None, + ); assert!(rendered.contains("hyperhive agent `alice`")); assert!(rendered.contains("**they/them** pronouns")); assert!(!rendered.contains("{label}")); @@ -279,7 +285,13 @@ shared closer #[test] fn render_no_role_markers_in_output() { // No raw role markers should survive into the rendered prompt. - let rendered = render(&PRODUCTION_TEMPLATE, "alice", "she/her", None, None); + let rendered = render( + &PRODUCTION_TEMPLATE, + "alice", + "she/her", + None, + None, + ); assert!(!rendered.contains("