# Agent roster & privileges Agents are a **flat set**, with no parent/child tree: the `parent` field `topology.json` used to carry is gone, along with every mechanism that read it. The capability store scopes which agents can manage which others; a tree position no longer scopes anything. This doc covers what the roster file is now, what the removal took with it, and where the manager still gets special-cased, as a tracked cleanup. ## Where the roster lives "The roster" means two different, non-overlapping things depending on scope. The [README](../../README.md)'s control-plane row ("`swarm-controller` holds the hive directory, the agent roster and the job graph") is the **swarm-wide** one: `GET /api/agents` on swarm-controller reads straight from the swarm's authelia identity store over `swarm-authelia-bridge` (`swarm-controller/src/auth.rs::list_agent_identities`) — "every agent the swarm holds an identity for" (`swarm-controller/src/main.rs`'s `get_agents` doc comment). It's authoritative: an agent exists in the swarm if and only if it has an identity there, and `swarmctl agent create` (or the swarm UI, via `POST /api/agents`) writes that identity once, by calling `ensure_agent_identity`. This page is about a different, **hive-local** file: the hive-c0re-owned **meta repo**, alongside `flake.nix`, at `/var/lib/hyperhive/meta/topology.json`: ```json ["alice", "bob", "ruth"] ``` One entry per agent _this hive_ currently has state/config for, in name order. Unlike the swarm roster, nothing writes this file directly — it's a derived cache, rebuilt by the reconcile pass below from what the hive observes locally (config repos cloned, containers spawned), and it answers a narrower question than "does this agent exist": _which of this hive's local agents the `ManageRootAgent` capability's bind-mounts should cover._ `topology::all_agents` is the only reader that matters. That reader is a permission boundary: the set it returns is what an agent holding the `ManageRootAgent` capability gets bind-mounted (each other agent's `state` read-write and `config` read-only; never `harness`). An agent holding no capability sees its own dirs and nothing else. See [`persistence.md`](persistence.md)'s _Cross-agent access to state._ ### Reading the legacy format `topology.json` used to be a map of `name → parent | null`. The reader still accepts that shape and keeps its keys, so a hive upgrading across the change reads the same roster rather than an empty one. An empty roster costs more than a cosmetic gap: every capability holder loses its mounts until the next reconcile pass writes the array form. ### Why meta, not per-agent `agent.nix` An agent shouldn't be able to add itself to a set that governs who may reach its state dir. The roster IS a system-level fact; meta is where system-level facts live. ### How `topology.json` gets updated - **Read** — parsed into a set of names; a missing or unparsable file degrades safely to "no agents" (covers a fresh install that hasn't synced yet). - **Reconcile** — runs alongside the periodic meta/flake regeneration. Adds newly-spawned agents, drops removed ones. Reconcile keeps agents whose config repo exists but that haven't spawned yet, so the gap until the container appears doesn't churn the file. No write API and no operator verb reach this file. Reconcile derives it from which agents exist, so the next pass overwrites a hand edit. See `hive-c0re/src/agent_config/topology.rs` and `hive-c0re/src/meta.rs`'s module docs for the exact call chain. ## What the parent field used to do Recorded so a reader who finds one of these in an old branch, an issue thread or a stale comment knows each one went away rather than moved: | gone | what replaced it | | -------------------------------------------- | --------------------------------------------------- | | `` recipient sentinel | address `operator` directly | | `` fan-out recipient | nothing — name the recipients, or broadcast to `*` | | `hivectl agent set-parent` | nothing | | `POST /api/topology/set-parent{,-bulk}` | nothing | | `HostRequest::SetParent` | nothing | | `NodeKind::Reparent` and its DAG template | nothing | | `HIVE_PARENT` on the container | nothing — no consumer ever read it | | every agent's grant over its direct children | the `ManageRootAgent` capability, for every agent | | rebuild ordering by topology depth | alphabetical, which the depth sort already produced | The last row is the one with teeth: an agent that used to reach a child's state dir by virtue of being its parent no longer reaches it at all unless it holds `ManageRootAgent`. That narrowing is the intended consequence of removing the field, not a side effect of it. ## Manager special-casing today Capability enforcement isn't fully wired yet, so the **manager (`ruth`) still gets some hard-coded special treatment** other agents don't. A hive can opt out of having one at all (`services.hyperhive.ruthless = true` skips `hive-c0re`'s root-agent create/start sweep entirely, `hive-c0re/src/workers/auto_update.rs`); everything below applies only when it doesn'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 is created at swarm level (`swarmctl agent create`). Roster-wise, `ruth` is just another entry. - **Wire-protocol** — the only `*(privileged)*` `Request` variants left in `hive-core-agent-sock`'s unified enum are the scheduling ops (`RequestSchedulePrompt`, `CancelSchedule`, `FireScheduleNow`, `EditSchedule`), reachable only from the manager's socket flavour today, matching the `scheduling` tool group ([`docs/turn-loop/mcp.md`](../turn-loop/mcp.md)). Container lifecycle ops (kill/start/restart/rebuild) never lived on this socket — they go through the separate host-admin socket `hivectl` speaks. `Wake` (inject a `from: ` message straight into the caller's own inbox) is on this socket too but isn't privileged to either flavour; no built-in in-container producer calls it today — matrix, bash and forge notifications all moved to pushing a todo on the harness's in-agent socket instead (see [`docs/turn-loop/mcp.md`](../turn-loop/mcp.md#waking-the-agent-from-inside-the-container)). - **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). That grant is the `ManageRootAgent` capability now, and ruth holds it; no name check remains. hive-c0re will gate RO `/meta` access on a "meta read" capability; no agent-facing path writes `flake.lock` any more — `request_update_meta_inputs` was removed, leaving the operator dashboard's `POST /api/meta-update` as the only entry point. - **Prompt** — _not_ treated differently in practice any more: `prompts/system.md` still carries `` / `` marker blocks, but `prompt::render` filters for `"agent"` unconditionally for every container, manager included ("always `agent` role — there is only one role", `hive-agent/src/prompt.rs`'s own module doc). The `role:manager` blocks are dead in production, exercised only by a unit test (`filter_role_blocks(SAMPLE, "manager")`). - **Tool allow-list** — also not a flavour switch: the MCP tools claude sees come from `HIVE_TOOL_GROUPS` alone, the same mechanism for every agent ([`docs/turn-loop/mcp.md`](../turn-loop/mcp.md)). Ruth's wider default surface is just a wider default grant (`ToolGroup::MANAGER_DEFAULT`, seeded by `auto_update.rs` whenever its groups aren't already set), not anything keyed off its name or container. - **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. None of the above is a stable interface — treat the module doc comments as the source of truth for exactly which checks exist today. ## 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 `services.hyperhive.agent.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 `services.hyperhive.agent.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 (`services.hyperhive.agent.user.passwordlessSudo`). ## Cross-references - Milestone: "Agent privileges and sub-agents" (tracked internally) - Audit table source: milestone comment (tracked internally) - Operator/agent trust boundary (orthogonal axis): [`boundary.md`](../trust-boundary/boundary.md)