Moves the "Harness systemd unit shape" section from docs/agent-lifecycle/agent-roster.md into docs/turn-loop/README.md: it describes the per-agent harness systemd unit (env vars, PATH wiring, serviceConfig), which is turn-loop material, not roster material. Fixes two facts while moving: the ExecStart package is `hive-agent`, not `hyperhive` (no package by that name exists); and `ruth.nix` doesn't set any forge subscription default — it only defaults `services.hyperhive.agent.docs.enable`. Updates the inbound pointers in docs/turn-loop/config.md and the module comment at nix/agent-modules/agent-service.nix. Refs #3902
143 lines
6.9 KiB
Markdown
143 lines
6.9 KiB
Markdown
# Agent roster & privileges
|
|
|
|
<!-- vale write-good.Passive = NO -->
|
|
|
|
Agents are a **flat set**: the capability store 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/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_ 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` 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:
|
|
`ManageRootAgent` is the only grant that reaches another agent's state
|
|
dir. See
|
|
[`persistence.md`](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 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.
|
|
|
|
## Hard-coded manager behaviour
|
|
|
|
The **manager (`ruth`)** differs from other agents in naming/bootstrap,
|
|
its socket flavour and its default capability grant; its prompt, tool
|
|
allow-list and state dirs work as for every other agent. A hive with
|
|
`services.hyperhive.ruthless = true` runs no manager: `hive-c0re` skips
|
|
its root-agent create/start sweep
|
|
(`hive-c0re/src/workers/auto_update.rs`), and this section doesn't
|
|
apply.
|
|
|
|
<!-- 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 `*(privileged)*` `Request` variants in
|
|
`hive-core-agent-sock`'s unified enum are the scheduling ops
|
|
(`RequestSchedulePrompt`, `CancelSchedule`, `FireScheduleNow`,
|
|
`EditSchedule`), reachable only from the manager's socket flavour,
|
|
matching the `scheduling` tool group
|
|
([`docs/turn-loop/mcp.md`](../turn-loop/mcp.md)). 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/turn-loop/mcp.md`](../turn-loop/mcp.md#waking-the-agent-from-inside-the-container)).
|
|
- **Storage/mounts** — 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: ruth
|
|
holds it by default, and any agent holding it gets the same mounts.
|
|
The operator dashboard's `POST /api/meta-update` is the only path
|
|
that writes `flake.lock`.
|
|
- **Prompt** — identical for every container: `prompts/system.md`
|
|
carries `<!-- role:agent -->` / `<!-- role:manager -->` marker
|
|
blocks, and `prompt::render` filters for `"agent"` unconditionally,
|
|
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** — 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 a wider default grant (`ToolGroup::MANAGER_DEFAULT`, seeded
|
|
by `auto_update.rs` whenever its groups aren't already set).
|
|
- **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.
|
|
|
|
## 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)
|