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:
parent
d94bc2188d
commit
179f873722
17 changed files with 186 additions and 262 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -483,12 +483,12 @@ approval card. See `docs/web-ui/dashboard.md`.
|
|||
|
||||
### Submitting agent's view of config repos
|
||||
|
||||
Every parent agent's container has its **direct children's** config
|
||||
repos bind-mounted **read-only** (topology-driven:
|
||||
`hive-c0re/src/lifecycle/host_config.rs` calls `bind_child_agent_dirs`
|
||||
for each entry in
|
||||
`topology::children_of(agent_name)`). it's a copy to *read* a child's
|
||||
current config — not an editing surface.
|
||||
An agent holding `ManageRootAgent` has every other agent's config repo
|
||||
bind-mounted **read-only** (`hive-c0re/src/lifecycle/host_config.rs`
|
||||
calls `bind_child_agent_dirs` for each entry in
|
||||
`topology::all_agents()`). it's a copy to *read* another agent's
|
||||
current config — not an editing surface. An agent without the
|
||||
capability sees no other agent's config at all.
|
||||
|
||||
An agent with the `approvals` tool group submits a change the same way
|
||||
it makes any other change: **clone the child's config repo from the
|
||||
|
|
|
|||
|
|
@ -335,24 +335,29 @@ Under `/var/lib/hyperhive/agents/<name>/`:
|
|||
- `hyperhive-turn-stats.sqlite` — per-turn timing stats.
|
||||
- `hyperhive-model` — single-line model name override file.
|
||||
|
||||
### Parent access to child state
|
||||
### Cross-agent access to state
|
||||
|
||||
A parent agent gets each direct child's `state` dir bind-mounted
|
||||
**read-write** and its `config` dir **read-only**
|
||||
(`bind_child_agent_dirs` in `lifecycle/host_config.rs`). The RW on
|
||||
`state` is deliberate, not an oversight: a parent manages its children,
|
||||
which includes writing into a child's state for recovery (for example seeding
|
||||
notes, clearing a stuck sentinel) as well as reading it.
|
||||
An agent holding the `ManageRootAgent` capability gets every other
|
||||
agent's `state` dir bind-mounted **read-write** and its `config` dir
|
||||
**read-only** (`bind_child_agent_dirs` in `lifecycle/host_config.rs`).
|
||||
The RW on `state` is deliberate, not an oversight: the holder recovers
|
||||
other agents, which includes writing into their state (for example
|
||||
seeding notes, clearing a stuck sentinel) as well as reading it.
|
||||
|
||||
**`harness` isn't mounted at all.** It holds the child's own runtime
|
||||
This is the **only** cross-agent mount. #4472 removed the topology
|
||||
parent field, and with it the unconditional grant every agent used to
|
||||
get over its own direct children — an agent holding no capability now
|
||||
sees its own dirs and nothing else.
|
||||
|
||||
**`harness` isn't mounted at all.** It holds that agent's own runtime
|
||||
material — `bash-tasks/`, the turn-stats and event sqlite dbs — and
|
||||
nothing argues for a parent reading it, let alone writing it. hive-c0re
|
||||
reads a child's harness dir **directly on the host** when it wants
|
||||
those stats, which needs no mount into the parent.
|
||||
nothing argues for anyone else reading it, let alone writing it.
|
||||
hive-c0re reads a harness dir **directly on the host** when it wants
|
||||
those stats, which needs no mount into another container.
|
||||
|
||||
<!-- vale write-good.Passive = NO -->
|
||||
**`config` is read-only, including for the parent.** A config change is
|
||||
a PR on the child's config repo, made from a clone and merged after
|
||||
**`config` is read-only, including for the holder.** A config change is
|
||||
a PR on that agent's config repo, made from a clone and merged after
|
||||
review — so the bind-mounted `config` dir is a *copy to read*, never a
|
||||
tree anyone edits in place. Mounting it writable would leave a second
|
||||
path to the same file that skips the review entirely, which makes the
|
||||
|
|
@ -361,15 +366,15 @@ boundary a convention rather than a permission.
|
|||
|
||||
<!-- vale write-good.Passive = NO -->
|
||||
⚠️ Don't confuse it with the config-repo seeding hive-c0re does at
|
||||
spawn (`lifecycle::setup_proposed`): that writes the child's initial
|
||||
spawn (`lifecycle::setup_proposed`): that writes the agent's initial
|
||||
config repo as **hive-c0re, against the host path**, and `read_only` on a bind
|
||||
constrains writers *inside* a container only. The two are unrelated —
|
||||
conflating them can lead you to reason your way into thinking this
|
||||
mount should be writable when it shouldn't.
|
||||
<!-- vale write-good.Passive = YES -->
|
||||
|
||||
Per-child isolation still holds: a container only ever has its *own*
|
||||
dirs plus its direct children's bind-mounted, never a sibling's.
|
||||
Isolation still holds by default: a container has its *own* dirs and,
|
||||
unless it holds the capability, nothing else.
|
||||
|
||||
Under `/var/lib/hyperhive/applied/<name>/` — the hive-c0re-only
|
||||
applied repo. Tracks `flake.nix` (module-only boilerplate; never
|
||||
|
|
@ -387,13 +392,11 @@ Contents:
|
|||
`nixosConfigurations.<n>` output per agent. `flake.lock` is the
|
||||
canonical "what's deployed where." The git log is the deploy
|
||||
audit trail (one commit per successful deploy or hyperhive bump).
|
||||
- `topology.json` — parent/child agent graph
|
||||
(`{ "alice": "root", "bob": "alice", "root": null }`).
|
||||
Written by `topology::apply_set_parent` (the pure move-validating
|
||||
transform) via `meta::bulk_commit_topology` (the committer — see the
|
||||
`Reparent` node in [`docs/scheduler/coordinator.md`](../scheduler/coordinator.md)); read by
|
||||
the dashboard, the renderer, and `<parent>` / `<children>` recipient
|
||||
resolution.
|
||||
- `topology.json` — the agent roster (`["alice", "bob", "ruth"]`).
|
||||
Written by `topology::reconcile` on every meta sync; read by
|
||||
`topology::all_agents`, which is the set the `ManageRootAgent`
|
||||
capability grants mounts over. Carried a `parent` per agent until
|
||||
#4472; the reader still accepts that shape and keeps its keys.
|
||||
- `tool-groups.json` — per-agent MCP tool group grants
|
||||
(`{ "alice": ["messaging", "inbox", "execution"] }`). Written by
|
||||
`tool_groups::set_groups`; injected as `HIVE_TOOL_GROUPS` env
|
||||
|
|
|
|||
Loading…
Reference in a new issue