Agent roster & privileges
Agents are a flat set: the manage_root_agent permission scopes
which agents can manage which others.
This doc covers what the roster file holds, the map-shaped alternative
topology::all_agents accepts, and what's hard-coded for the manager.
Where the roster lives
"The roster" means two different, non-overlapping things depending on
scope. The README'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/routes/agents.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:
["alice", "bob", "ruth"]
One entry per agent this hive has state/config for, in name order.
Unlike the swarm roster, it's a derived cache: the reconcile pass below
rebuilds it 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 permission'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 permission gets bind-mounted (each other
agent's state read-write and config read-only; never harness). An
agent holding no permission sees its own dirs and nothing else:
ManageRootAgent is the only grant that reaches another agent's state
dir. See
persistence.md's Cross-agent access to state.
Reading the map-shaped format
topology.json may also be a map of name → parent | null; the reader
accepts that shape and takes its keys as the roster. An empty
roster costs more than a cosmetic gap: every permission 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.
Hard-coded manager behaviour
The manager (ruth) differs from other agents in naming/bootstrap
and its socket flavour; its permissions, prompt, tool allow-list and
state dirs work as for every other agent.
- Naming/bootstrap — the manager's broker recipient name, state-dir
key, and nixos-container name are all
ruth(containerh-ruth). Like every agent it comes from the swarm dashboard orswarmctl agent create;hive-c0recreates nothing at boot. Roster-wise,ruthis just another entry. - Wire-protocol —
hive-core-agent-sock's unifiedRequestenum is the same on both socket flavours. Container lifecycle ops (kill/start/restart/rebuild) go through the separate host-admin sockethivectlspeaks.Wake(inject afrom: <X>message straight into the caller's own inbox) is on this socket too, unprivileged on both flavours; matrix, bash and forge don't call it — their notifications push a todo on the harness's in-agent socket (seedocs/turn-loop/mcp.md). - Storage/mounts — the
manage_root_agentpermission, which the swarm UI's setup assistant creates ruth with, gets a container/var/lib/hyperhive/agentsbind-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). Any agent holding it gets the same mounts. The operator dashboard'sPOST /api/meta-updateis the only path that writesflake.lock. - Prompt — identical for every container but one block (below):
prompts/system.mdcarries<!-- role:agent -->/<!-- role:manager -->marker blocks, andprompt::renderfilters for"agent"unconditionally, manager included ("alwaysagentrole — there is only one role",hive-agent/src/prompt.rs's own module doc). Therole:managerblocks are dead in production, exercised only by a unit test (filter_blocks(SAMPLE, "role", "manager")). The one block that differs is<!-- config:repo -->vs<!-- config:archetype -->: an agent built from an archetype is told it has no config repo. - Tool allow-list — the MCP tools claude sees come from the agent's
permissions alone, the same mechanism for every agent
(
docs/turn-loop/mcp.md). Ruth's permissions are the default plusmanage_root_agent, stored for her like any grant. - State dirs — not special-cased:
HYPERHIVE_STATE_DIRis injected uniformly viasystemd.globalEnvironmentfor 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.
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