Watch
0
0
Fork
You've already forked hyperhive
0

docs: the agent roster page keeps the operator view and moves the topology detail out

The page says who is on the roster, who may reach whose state and that the
manager is a normal agent. How topology.json is read and rebuilt, the wire
protocol and the prompt blocks move to 4-implementation/agent-state/roster.md,
unchanged, since the code they describe still exists.
This commit is contained in:
iris 2026-11-11 03:05:29 +01:00 • committed by mara
commit e746ed1888
4 changed files with 156 additions and 121 deletions

View file

@ -1,128 +1,35 @@
# Agent roster & privileges
# Agent roster and privileges
<!-- vale write-good.Passive = NO -->
Agents are a **flat set**: no agent manages another by position. The
`manage_root_agent` permission is the one grant that reaches another agent.
Agents are a **flat set**: the `manage_root_agent` permission scopes
which agents can manage which others.
## Who is on the roster
<!-- vale write-good.Passive = YES -->
The swarm decides. An agent exists in the swarm if and only if it has an identity
in the swarm's authelia identity store, which `swarmctl agent create` or the
swarm UI writes once. `GET /api/agents` on the swarm controller lists them.
This doc covers what the roster file holds, the map-shaped alternative
`topology::all_agents` accepts, and what's hard-coded for the manager.
Each hive also keeps a small file of its own, `topology.json`, with the agents
this hive has state or config for. You never edit it: the hive rebuilds it from
what it sees locally, so a hand edit is overwritten, and one that doesn't parse
stops the rebuild until you fix it. It matters for one thing, covered next.
## Where the roster lives
## Who may reach whose state
"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/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`.
An agent holding no permission sees its own directories and nothing else. An
agent holding `manage_root_agent` gets every other agent's `state` directory on
this hive mounted read-write at `/agents/<name>/state`, never `config` or
`harness`. It's the only grant that reaches another agent's state. Ruth, the
root agent, holds it, and the swarm UI's setup assistant creates her with it. The
mounts follow the roster: an empty file means no mounts until the next rebuild.
This page is about a different, **hive-local** file: the hive-c0re-owned
`meta/` directory's `topology.json`, at
`/var/lib/hyperhive/meta/topology.json`:
## The manager is a normal agent
```json
["alice", "bob", "ruth"]
```
`ruth` differs from other agents in her name and in that grant. You create her
like any other agent, from the swarm UI or `swarmctl agent create`; the hive
creates nothing at boot. Her permissions, prompt and tools work as for every
agent, and destroying her is as final as for any other.
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` dir, read-write, at `/agents/<name>/state`; never `config` or
`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`](../../4-implementation/agent-state/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. The live set is the
hive's existing agent containers plus the agent that spawns now; reconcile
drops an agent whose config repo exists but whose container doesn't.
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 that
parses. It refuses to write over a file it can't parse and fails
instead, so a broken hand edit stays until someone fixes it.
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.
<!-- vale write-good.Passive = NO -->
- **Naming/bootstrap** — the manager's broker recipient name, state-dir
key, and nixos-container name are all `ruth` (container `h-ruth`).
Like every agent it comes from the swarm dashboard or
`swarmctl agent create`; `hive-c0re` creates nothing at boot.
Roster-wise, `ruth` is just another entry.
- **Wire-protocol** — `hive-core-agent-sock`'s unified `Request` enum
is the same on both socket flavours. Container lifecycle
ops (kill/start/restart/rebuild) go through the separate host-admin
socket `hivectl` speaks. `Wake` (inject a `from: <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 (see
[`docs/4-implementation/turn-loop/mcp.md`](../../4-implementation/turn-loop/mcp.md#waking-the-agent-from-inside-the-container)).
- **Storage/mounts** — the `manage_root_agent` permission, which the swarm
UI's setup assistant creates ruth with, gets a container
each other agent's `state` dir bind-mounted RW at
`/agents/<name>/state` (so it can manage any agent's state dir; config
isn't mounted, since a real config change is a PR from a clone), plus an RO mount for
`/meta` (the hive's topology and limits files). Any agent holding it gets the same mounts.
- **Prompt** — identical for every container but one paragraph: an agent
built from an [archetype](../swarm/archetypes.md) is told it has no config
repo, every other agent is told how to propose changes to its own.
- **Tool allow-list** — the MCP tools claude sees come from the agent's
permissions alone, the same mechanism for every agent
([`docs/4-implementation/turn-loop/mcp.md`](../../4-implementation/turn-loop/mcp.md)). Ruth's permissions
are the default plus `manage_root_agent`, stored for her like any grant.
- **State dirs** — _not_ treated as an exception: `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.
<!-- vale write-good.Passive = YES -->
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`](../hives/boundary.md)
How the hive reads and rebuilds the file, the wire protocol and the prompt blocks:
[`roster.md`](../../4-implementation/agent-state/roster.md). The operator/agent
trust boundary is a separate axis: [`boundary.md`](../hives/boundary.md).

View file

@ -0,0 +1,126 @@
# Agent roster: topology file and manager behaviour
The hive-local `topology.json`, how it is derived, and what is special about the
manager (`ruth`). The operator's overview is
[the agent roster page](../../2-handbook/agents/agent-roster.md).
<!-- vale write-good.Passive = NO -->
Agents are a **flat set**: the `manage_root_agent` permission scopes
which agents can manage which others.
<!-- vale write-good.Passive = YES -->
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](../../../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/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/` directory's `topology.json`, at
`/var/lib/hyperhive/meta/topology.json`:
```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` dir, read-write, at `/agents/<name>/state`; never `config` or
`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`](../agent-state/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. The live set is the
hive's existing agent containers plus the agent that spawns now; reconcile
drops an agent whose config repo exists but whose container doesn't.
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 that
parses. It refuses to write over a file it can't parse and fails
instead, so a broken hand edit stays until someone fixes it.
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.
<!-- vale write-good.Passive = NO -->
- **Naming/bootstrap** — the manager's broker recipient name, state-dir
key, and nixos-container name are all `ruth` (container `h-ruth`).
Like every agent it comes from the swarm dashboard or
`swarmctl agent create`; `hive-c0re` creates nothing at boot.
Roster-wise, `ruth` is just another entry.
- **Wire-protocol** — `hive-core-agent-sock`'s unified `Request` enum
is the same on both socket flavours. Container lifecycle
ops (kill/start/restart/rebuild) go through the separate host-admin
socket `hivectl` speaks. `Wake` (inject a `from: <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 (see
[`docs/4-implementation/turn-loop/mcp.md`](../turn-loop/mcp.md#waking-the-agent-from-inside-the-container)).
- **Storage/mounts** — the `manage_root_agent` permission, which the swarm
UI's setup assistant creates ruth with, gets a container
each other agent's `state` dir bind-mounted RW at
`/agents/<name>/state` (so it can manage any agent's state dir; config
isn't mounted, since a real config change is a PR from a clone), plus an RO mount for
`/meta` (the hive's topology and limits files). Any agent holding it gets the same mounts.
- **Prompt** — identical for every container but one paragraph: an agent
built from an [archetype](../../2-handbook/swarm/archetypes.md) is told it has no config
repo, every other agent is told how to propose changes to its own.
- **Tool allow-list** — the MCP tools claude sees come from the agent's
permissions alone, the same mechanism for every agent
([`docs/4-implementation/turn-loop/mcp.md`](../turn-loop/mcp.md)). Ruth's permissions
are the default plus `manage_root_agent`, stored for her like any grant.
- **State dirs** — _not_ treated as an exception: `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.
<!-- vale write-good.Passive = YES -->
None of the above is a stable interface — treat the module doc
comments as the source of truth for exactly which checks exist today.

View file

@ -555,4 +555,4 @@ config — all nspawn containers live in `machine.slice`.
- `docs/4-implementation/agent-state/config-changes.md` — config PRs, meta flake, system messages
- `docs/4-implementation/agent-state/persistence.md` — SQLite schema, state-dir layout
- `docs/5-project/process/conventions.md` — wire protocol, recipient sentinels
- `docs/2-handbook/agents/agent-roster.md` — the agent roster and privilege
- `docs/4-implementation/agent-state/roster.md` — the agent roster and privilege

View file

@ -98,7 +98,9 @@ One agent: its roster entry, state, config and page. Start at
tables and directory layout:
[`4-implementation/agent-state/persistence.md`](4-implementation/agent-state/persistence.md).
- **Who can do what to whom — the agent roster and privilege?** →
[`2-handbook/agents/agent-roster.md`](2-handbook/agents/agent-roster.md).
[`2-handbook/agents/agent-roster.md`](2-handbook/agents/agent-roster.md); how
`topology.json` is rebuilt and what is special about the manager:
[`4-implementation/agent-state/roster.md`](4-implementation/agent-state/roster.md).
- **What does an agent's own page show?** →
[`2-handbook/agents/web-page.md`](2-handbook/agents/web-page.md); the full
reference is [`agent-page`](4-implementation/web-ui/agent-page.md).