mcp.md: - matrix and subagent extra MCP servers are http (hive-matrix-daemon, hive-subagent-daemon), not stdio; screen is the one entry that still uses the stdio default (nix/agent-modules/matrix.nix:290-297, screen.nix:22-25) - set_status is always-on, not meta-group-gated; mark_todos_done (also always-on) was undocumented (hive-sh4re/src/permissions.rs:151, hive-agent-mcp/src/mcp/mod.rs:425) - System messages: HelperEvent has 3 variants, not the 8 previously listed; ApprovalResolved/ContainerCrash routing and the swarm-wide NATS notices stream (swarm_notices.rs) replace the old per-agent todo-wake description for rebuilt/killed/destroyed/logged_in/needs_login - get_loose_ends's approval rows are manager-only; PendingMessages and UnreadMatrix were missing from the description (hive-sh4re/src/inbox.rs:127-184) - subagent spawning runs on hive-runtime (claude or ACP), not claude-only (hive-subagent-mcp/src/session.rs:77) - Waking section: matrix/bash/forge all moved to the in-agent todo socket; the host Wake request has no built-in caller left today agent-hierarchy.md: - distinguished the swarm-wide agent roster (swarm-controller's identity store, authoritative) from the hive-local topology.json (a derived, reconciled cache scoping ManageRootAgent's bind-mounts), linking README's framing - noted services.hyperhive.ruthless (a hive can run with no manager at all) - Wire-protocol bullet: the only privileged Request variants left are the scheduling ops; Kill/Start/Restart/Update/GetLogs don't exist on this socket - Prompt/tools: prompt::render hardcodes the agent role for every container today (role:manager blocks are dead code); the tool allow-list has no Flavor switch, it's HIVE_TOOL_GROUPS same as any agent Not touched: agent-hierarchy.md:140-200 (Harness systemd unit shape, kept in place — see PR follow-ups) and docs/agent-lifecycle/approvals.md (blocked on #4853).
240 lines
12 KiB
Markdown
240 lines
12 KiB
Markdown
# Agent roster & privileges
|
|
|
|
<!-- vale write-good.Passive = NO -->
|
|
|
|
Agents are a **flat set**, with no parent/child tree: the `parent` field
|
|
`topology.json` used to carry is gone, along with 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" 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/main.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`:
|
|
|
|
```json
|
|
["alice", "bob", "ruth"]
|
|
```
|
|
|
|
One entry per agent _this hive_ currently has state/config for, in name
|
|
order. Unlike the swarm roster, nothing writes this file directly —
|
|
it's a derived cache, rebuilt by the reconcile pass below 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` capability'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` 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 legacy 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. A hive can opt out of having one at all
|
|
(`services.hyperhive.ruthless = true` skips `hive-c0re`'s root-agent
|
|
create/start sweep entirely, `hive-c0re/src/workers/auto_update.rs`);
|
|
everything below applies only when it doesn'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 is created at swarm level
|
|
(`swarmctl agent create`).
|
|
Roster-wise, `ruth` is just another entry.
|
|
- **Wire-protocol** — the only `*(privileged)*` `Request` variants left
|
|
in `hive-core-agent-sock`'s unified enum are the scheduling ops
|
|
(`RequestSchedulePrompt`, `CancelSchedule`, `FireScheduleNow`,
|
|
`EditSchedule`), reachable only from the manager's socket flavour
|
|
today, matching the `scheduling` tool group
|
|
([`docs/turn-loop/mcp.md`](../turn-loop/mcp.md)). Container lifecycle
|
|
ops (kill/start/restart/rebuild) never lived on this socket — they 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 but isn't privileged to either flavour; no
|
|
built-in in-container producer calls it today — matrix, bash and
|
|
forge notifications all moved to pushing a todo on the harness's
|
|
in-agent socket instead (see
|
|
[`docs/turn-loop/mcp.md`](../turn-loop/mcp.md#waking-the-agent-from-inside-the-container)).
|
|
- **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** — _not_ treated differently in practice any more: `prompts/system.md`
|
|
still carries `<!-- role:agent -->` / `<!-- role:manager -->` marker
|
|
blocks, but `prompt::render` filters for `"agent"` unconditionally for
|
|
every container, manager included ("always `agent` role — there is
|
|
only one role", `hive-agent/src/prompt.rs`'s own module doc). The
|
|
`role:manager` blocks are dead in production, exercised only by a
|
|
unit test (`filter_role_blocks(SAMPLE, "manager")`).
|
|
- **Tool allow-list** — also not a flavour switch: the MCP tools claude
|
|
sees come from `HIVE_TOOL_GROUPS` alone, the same mechanism for every
|
|
agent ([`docs/turn-loop/mcp.md`](../turn-loop/mcp.md)). Ruth's wider
|
|
default surface is just a wider default grant
|
|
(`ToolGroup::MANAGER_DEFAULT`, seeded by `auto_update.rs` whenever its
|
|
groups aren't already set), not anything keyed off its name or
|
|
container.
|
|
- **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.
|
|
<!-- 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)
|