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.
218 lines
10 KiB
Markdown
218 lines
10 KiB
Markdown
# Agent roster & privileges
|
|
|
|
<!-- vale write-good.Passive = NO -->
|
|
|
|
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 -->
|
|
|
|
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 lives in the hive-c0re-owned **meta repo**, alongside
|
|
`flake.nix`, at `/var/lib/hyperhive/meta/topology.json`:
|
|
|
|
```json
|
|
["alice", "bob", "ruth"]
|
|
```
|
|
|
|
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.
|
|
|
|
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 pre-#4472 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 |
|
|
| -------------------------------------------- | --------------------------------------------------- |
|
|
| `<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 |
|
|
|
|
<!-- vale write-good.Passive = NO -->
|
|
|
|
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
|
|
|
|
Capability enforcement isn't fully wired yet, so the
|
|
**manager (`ruth`) still gets some hard-coded special treatment**
|
|
other agents don't:
|
|
|
|
<!-- 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`).
|
|
`hive-c0re` spawns it directly at boot if missing, with no operator
|
|
approval step — every other agent goes through a `Spawn` approval.
|
|
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; 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.
|
|
- **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/tools** — the system prompt uses `<!-- role:agent -->` /
|
|
`<!-- role:manager -->` marker blocks, and a `Flavor::{Agent,
|
|
Manager}` switch picks the MCP tool allow-list claude sees. Both are
|
|
already parametrised on a single flavour value, so the planned
|
|
per-capability-group version (`cap:<group>` prompt blocks + a
|
|
matching tool allow-list) is additive rather than a rewrite.
|
|
- **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.
|
|
- **Scattered ownership checks** — a handful of independent
|
|
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 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
|
|
the note on `CancelLooseEndKind::Reminder` in
|
|
`hive-c0re/src/socket_server/mod.rs`.)
|
|
<!-- 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.
|
|
|
|
## 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/<userName>` — 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 = <mergedDist>` — `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" ];
|
|
```
|
|
|
|
<!-- vale write-good.Passive = NO -->
|
|
|
|
`/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."
|
|
|
|
<!-- vale write-good.Passive = YES -->
|
|
|
|
### `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)
|