# Agent hierarchy & privileges Every agent has a place in an operator-editable parent/child tree, used to scope which agents can manage which others. This doc covers how hive-c0re stores and edits the tree today, the rules that are meant to run on top of it once enforcement is finished, and where the manager still gets special-cased in the meantime. Tracking issue: hyperhive#361 (`$HIVE_FORGE_URL/hyperhive/hyperhive/issues/361`). ## Where the tree lives Topology lives in the hive-c0re-owned **meta repo**, alongside `flake.nix`, at `/var/lib/hyperhive/meta/topology.json`: ```json { "ruth": null, "alice": null, "bob": "alice" } ``` `null` = root-level agent. New agents **default to root** — there is no structural manager that everything hangs under. Hierarchy is built explicitly: an agent that requests a sub-agent gets a requester-as-parent edge written at its `init_config` approval (so `alice` spawned `bob` above), and the operator can reparent any agent, including the bootstrap container (`ruth`) — it's just another root. The manager is reparentable like any other agent; there's no "structurally root" carve-out. Its privileges live on its MCP socket, not its tree position (see _Manager special-casing today_ below). ### Reparenting - CLI: `hivectl agent set-parent --parent ` (or `--root` to promote). Exactly one of `--parent` / `--root` is required. - Dashboard: `POST /api/topology/set-parent` (form fields `child`, optional `new_parent` — absent / empty ⇒ promote to root). - Wire: `HostRequest::SetParent { child, new_parent: Option }`. All three go through the same validation, which refuses: - unknown `child` / `new_parent` (typo guard), - self-parenting, - cycles (a bounded ancestor walk — moving the manager under one of its own descendants is the only real safety concern here, and it's caught the same way as any other agent). Setting a parent to its current value is a no-op (no disk write). A successful change triggers an immediate rescan, so connected dashboard viewers see the tree repaint without polling. ### Why meta, not per-agent `agent.nix` An agent shouldn't be able to claim a parent without that parent's consent, and operator-driven re-parenting shouldn't require touching the moved agent's config. Topology IS a system-level concern; meta is where system-level facts live. ### How `topology.json` gets updated - **Read** — parsed into an agent→parent map; a missing or unparsable file degrades safely to "every agent is root" (covers a fresh install that hasn't synced yet). - **Reconcile** — runs alongside the periodic meta/flake regeneration. New agents default to root unless they already carry an explicit parent edge from an `init_config` approval; Reconcile preserves existing entries (including operator overrides); removed agents drop. Agents that are approved but not yet spawned keep their edge too, so it survives the gap until the container actually appears. - **Inject** — hive-c0re exposes each container's parent (if any) to its own environment as `HIVE_PARENT`, so the harness / system-prompt renderer can see it. - **Surface** — every rescan re-reads `topology.json` and populates `ContainerView.parent`, which the dashboard renders as a tree. See `hive-c0re/src/agent_config/topology.rs` and `hive-c0re/src/meta.rs`'s module docs for the exact call chain. ### Current limitation: state-dir visibility lags topology Reparenting today is purely a JSON edit. Only the top-level manager (`root`) gets `/var/lib/hyperhive/agents` bind-mounted at `/agents` in its container, so sub-agents don't yet see their would-be children's state dirs. Once sub-manager bind mounts land alongside capability enforcement, reparenting will grow a companion umount-old / mount-new / restart-cascade step. ## Planned topology semantics (once ancestor-based enforcement lands) | operation | who can do it | | ----------------------------------------------------------- | --------------------------------------------------------------------------------------- | | `kill` / `start` / `restart` / `update` (any descendant) | any ancestor | | `request_init_config` (spawn a new child) | any agent, child added under self | | config change via forge PR (any descendant's config) | any ancestor | | `get_logs` (any descendant) | any ancestor | | moderate reminders (cancel any open thread of a descendant) | any ancestor | | `send` / `recv` routing | parent ↔ same-parent siblings ↔ self ↔ descendants; explicit allow-list for anyone else | | `request_update_meta_inputs` (bump meta lock) | root agents only (today: just `manager`) | "Ancestor" walks `ContainerView.parent` chains; a visited-set guards against cycles at dispatch time (a malformed `topology.json` can't lock the dispatcher into a loop). ## Manager special-casing today Enforcement of the ancestor rules above isn't fully wired yet, so the **manager (`ruth`) still gets some hard-coded special treatment** other agents don't: - **Naming/bootstrap** — the manager's broker recipient name, state-dir key, and nixos-container name are all `ruth` (container `h-ruth`). `hive-c0re` spawns it directly at boot if missing, with no operator approval step — every other agent goes through `request_init_config` → approval. Topology-wise, `ruth` is still just another root agent. - **Wire-protocol** — the privileged `Request` variants (`RequestInitConfig`; `Kill` / `Start` / `Restart` / `Update`; `GetLogs`; `RequestUpdateMetaInputs`) — marked `*(privileged)*` in `hive-core-agent-sock`'s unified `Request` enum — are reachable only from the manager's socket flavour today. Planned rule for each is in the table above ("any agent, child added under self" for init-config, "any ancestor" for lifecycle/logs); `RequestUpdateMetaInputs` stays a root-only capability even post-milestone, not a topology rule. One exception: `Wake` (inject a `from: ` message into the caller's own inbox) isn't really privileged — every per-agent daemon (for example `hive-forge-notify`) needs it, and sub-agents already have the equivalent on their own socket. - **Storage/mounts** — only the manager container gets `/var/lib/hyperhive/agents` bind-mounted RW at `/agents` (so it can manage any agent's state dir — config isn't authored there, since a real config change is a PR from a clone), plus RO mounts for `/applied` (diff against what's deployed) and `/meta` (system-wide deploy log). Planned: each agent gets RW to `/agents//` for just its own subtree — the manager's full-forest RW becomes the "root's subtree is everything" case of that same rule. RO `/meta` access will be gated on a "meta read" capability; only `request_update_meta_inputs` writes `flake.lock`, gated by its own capability. - **Prompt/tools** — the system prompt uses `` / `` marker blocks, and a `Flavor::{Agent, Manager}` switch picks the MCP tool allow-list claude sees. Both are already parametrised on a single flavour value, so the planned per-capability-group version (`cap:` prompt blocks + a matching tool allow-list) is additive rather than a rewrite. - **State dirs** — _not_ special-cased: `HYPERHIVE_STATE_DIR` is injected uniformly via `systemd.globalEnvironment` for every container including the manager, so all token/state paths resolve through it the same way everywhere. - **Scattered ownership checks** — a handful of independent manager-only overrides exist across `hive-c0re` today: loose-ends visibility (manager sees hive-wide, sub-agents only their own), `destroy` refusing to act on the manager, and crash-watch skipping the manager (it autorestarts via systemd instead of going through the crash-watch loop). Each is planned to become an ancestor/descendant check instead of a manager-name check — see the module docs for `loose_ends.rs`, `stores/broker.rs`, `actions.rs`, and `workers/crash_watch.rs` for the current owner-check logic in each. (Reminder cancellation is handled fully in-agent — see the note on `CancelLooseEndKind::Reminder` in `hive-c0re/src/socket_server/mod.rs`.) None of the above is a stable interface — treat the module doc comments as the source of truth for exactly which checks exist today. ## Future work: sub-agents inside the same container When enabled for an agent, it will be able to spawn temporary "sub-agents" that run inside its own container — lighter than a full nspawn agent. Open questions, not yet wired: - Inherit caps from parent, or take an explicit narrower set? - Survive container restart, or always ephemeral? - Inbox: separate from parent, or shared? - Filesystem: share parent's `/state` RW, or a sub-dir? - Identity: distinct broker recipient name, or address the parent? ## Harness systemd unit shape One harness serve binary (`hive-agent`, with its `hive-agent-mcp` sibling), one shared `nix/agent-modules/` tree, one service unit (`systemd.services.hive-agent`) for all agents. No separate manager service name or role distinction exists in the harness — privilege differences live server-side in the broker socket (which tool groups and manager-surface calls each agent receives). `agent.nix` and `ruth.nix` both import the shared `nix/agent-modules/`. `ruth.nix` additionally sets forge defaults to suppress the subscription/participation firehose so ruth's inbox stays focused on direct mentions, reviews, and assignments. ### Environment variables set on the unit - `HOME = /home/` — systemd defaults `HOME` to `/` for services without `User=` set; with the per-agent user the harness needs the right home so claude finds its bind-mounted `~/.claude/` session dir. - `HIVE_STATIC_DIR = ` — `tower_http::ServeDir` root for the per-agent web UI; merged dist = agent default + every `hyperhive.frontend.extraFiles` overlay. - `HIVE_ASSETS_DIR = pkgs.hyperhive-assets/share/hyperhive` — set directly on the unit, **not** via `environment.variables`, because the latter only populates `/etc/profile` which systemd services don't inherit. ### `PATH` setup (the wrapper-dir trick) ```nix path = [ "/run/wrappers" "/run/current-system/sw" ]; ``` `/run/wrappers` (not `/run/wrappers/bin`) comes first so setuid wrappers — notably `sudo` — resolve before bare nix-store binaries; see [`docs/process/gotchas.md`](../process/gotchas.md) ("`systemd.services.*.path` appends `/bin` to every entry") for why the trailing `/bin` matters in general. It's load-bearing here because the harness runs as the per-agent user: without the wrapper dir on `PATH`, `sudo` resolves to the non-setuid nix-store binary and every `hyperhive.user.passwordlessSudo` grant fails with "must be owned by uid 0 and have the setuid bit set." ### `serviceConfig` highlights - `ExecStart = pkgs.hyperhive/bin/hive-agent` — same binary for every agent. - `Restart = on-failure`, `RestartSec = 2` — keeps the harness resilient across transient crashes without thundering retries. - `RuntimeDirectory = "hive-config"` → `/run/hive-config/` owned by `User=`, autocleared on stop. The harness writes regenerated `claude-{mcp-config,settings,system-prompt}` files there (`paths::config_dir`). Deliberately separate from `/run/hive`, which the host bind-mounts in root-owned and which holds hive-c0re's `mcp.sock`. - `User = Group = userName` — drops root inside the container; sudo is the explicit escalation surface (`hyperhive.user.passwordlessSudo`). ## Cross-references - Milestone: "Agent privileges and sub-agents" (`$HIVE_FORGE_URL/hyperhive/hyperhive/issues/361`) - Dashboard render: "show agent topology in container list" (`$HIVE_FORGE_URL/hyperhive/hyperhive/issues/363`) - Audit table source: milestone comment (`$HIVE_FORGE_URL/hyperhive/hyperhive/issues/361#issuecomment-3335`) - Operator/agent trust boundary (orthogonal axis): [`boundary.md`](../trust-boundary/boundary.md)