docs: retire the agent hierarchy from every page that described it

The topology doc keeps its filename and its second half (manager
special-casing, harness unit shape) — both are cross-referenced from
other pages and neither is about the parent field. Its first half is
rewritten: what topology.json is now, and a table of what the removal
took with it, so a reader who finds `<parent>` or `set-parent` in an old
issue thread learns it went away rather than moved.

The dashboard's tree-rendering section is marked dormant rather than
deleted: the walk is still in swarm.js and retiring it is the frontend
owner's call.
This commit is contained in:
atlas 2026-09-21 21:18:08 +02:00 committed by atlas
commit 179f873722
17 changed files with 186 additions and 262 deletions

View file

@ -1,109 +1,97 @@
# Agent hierarchy & privileges
# Agent roster & privileges
<!-- vale write-good.Passive = NO -->
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, as a tracked cleanup.
Agents are a **flat set**, with no parent/child tree: #4472 removed the
`parent` field `topology.json` used to carry, and every mechanism that
read it. The capability store scopes which agents can manage which
others; a tree position no longer scopes anything.
<!-- vale write-good.Passive = YES -->
## Where the tree lives
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.
Topology lives in the hive-c0re-owned **meta repo**, alongside
## Where the roster lives
The roster 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"
}
["alice", "bob", "ruth"]
```
`null` = root-level agent. New agents **default to root** — there is no
structural manager that everything hangs under. The operator builds
hierarchy explicitly: an agent gets a parent edge written before its
first spawn, or the operator reparents it afterwards (so `bob` above sits under
`alice`). Any agent is reparentable, the bootstrap container (`ruth`)
included — 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).
One entry per agent the hive knows about, in name order. The file
carries no per-agent value any more, and encodes no ordering or
grouping — it answers exactly one question, _which agents exist,_ and
`topology::all_agents` is the only reader that matters.
### Reparenting
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._
- CLI: `hivectl agent <child> set-parent --parent <new>` (or `--root`
to promote). Pass exactly one of `--parent` / `--root`.
- Dashboard: `POST /api/topology/set-parent` (form fields `child`,
optional `new_parent` — absent / empty ⇒ promote to root).
- Wire: `HostRequest::SetParent { child, new_parent: Option<String> }`.
### Reading the pre-#4472 format
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.
`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 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.
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 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).
- **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.
New agents default to root unless they already carry an explicit
parent edge written before their first spawn; Reconcile preserves
existing entries (including operator overrides); removed agents drop.
Agents whose config repo exists but that haven't spawned yet 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.
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.
### Current limitation: state-dir visibility lags topology
## What the parent field used to do
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.
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:
## Planned topology semantics (once ancestor-based enforcement lands)
| gone | what replaced it |
| -------------------------------------------- | --------------------------------------------------- |
| `<parent>` recipient sentinel | address `operator` directly |
| `<children>` fan-out recipient | nothing — name the recipients, or broadcast to `*` |
| `hivectl agent <name> 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 |
| operation | who can do it |
| ----------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| config change via forge PR (any descendant's config) | 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 |
<!-- vale write-good.Passive = NO -->
"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).
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.
<!-- vale write-good.Passive = YES -->
## Manager special-casing today
Enforcement of the ancestor rules above isn't fully wired yet, so the
Capability enforcement isn't fully wired yet, so the
**manager (`ruth`) still gets some hard-coded special treatment**
other agents don't:
@ -113,13 +101,12 @@ other agents don't:
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 a `Spawn` approval.
Topology-wise, `ruth` is still just another root agent.
Roster-wise, `ruth` is just another entry.
- **Wire-protocol** — the privileged `Request` variants
(`Kill` / `Start` / `Restart` / `Update`; `GetLogs`) — 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 ancestor" for
lifecycle/logs). One exception: `Wake` (inject a `from: <X>` message into the
are reachable only from the manager's socket flavour today; each is
planned to become a capability check. One exception: `Wake` (inject a `from: <X>` 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.
@ -128,9 +115,8 @@ other agents don't:
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/<descendant>/`
for just its own subtree — the manager's full-forest RW becomes the
"root's subtree is everything" case of that same rule. hive-c0re will
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
@ -150,8 +136,8 @@ 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
the crash-watch loop). Each is planned to become a capability
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. (The harness handles reminder cancellation fully in-agent — see
@ -162,18 +148,6 @@ the note on `CancelLooseEndKind::Reminder` in
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`
@ -240,6 +214,5 @@ uid 0 and have the setuid bit set."
## Cross-references
- Milestone: "Agent privileges and sub-agents" (tracked internally)
- Dashboard render: "show agent topology in container list" (tracked internally)
- Audit table source: milestone comment (tracked internally)
- Operator/agent trust boundary (orthogonal axis): [`boundary.md`](../trust-boundary/boundary.md)