diff --git a/docs/2-handbook/agents/agent-roster.md b/docs/2-handbook/agents/agent-roster.md index 676756742..200a4e499 100644 --- a/docs/2-handbook/agents/agent-roster.md +++ b/docs/2-handbook/agents/agent-roster.md @@ -1,128 +1,35 @@ -# Agent roster & privileges +# Agent roster and privileges - +Agents are a **flat set**: no agent manages another by position. The +`manage_root_agent` permission is the one grant that reaches another agent. -Agents are a **flat set**: the `manage_root_agent` permission scopes -which agents can manage which others. +## Who is on the roster - +The swarm decides. An agent exists in the swarm if and only if it has an identity +in the swarm's authelia identity store, which `swarmctl agent create` or the +swarm UI writes once. `GET /api/agents` on the swarm controller lists them. -This doc covers what the roster file holds, the map-shaped alternative -`topology::all_agents` accepts, and what's hard-coded for the manager. +Each hive also keeps a small file of its own, `topology.json`, with the agents +this hive has state or config for. You never edit it: the hive rebuilds it from +what it sees locally, so a hand edit is overwritten, and one that doesn't parse +stops the rebuild until you fix it. It matters for one thing, covered next. -## Where the roster lives +## Who may reach whose state -"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/routes/agents.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`. +An agent holding no permission sees its own directories and nothing else. An +agent holding `manage_root_agent` gets every other agent's `state` directory on +this hive mounted read-write at `/agents//state`, never `config` or +`harness`. It's the only grant that reaches another agent's state. Ruth, the +root agent, holds it, and the swarm UI's setup assistant creates her with it. The +mounts follow the roster: an empty file means no mounts until the next rebuild. -This page is about a different, **hive-local** file: the hive-c0re-owned -`meta/` directory's `topology.json`, at -`/var/lib/hyperhive/meta/topology.json`: +## The manager is a normal agent -```json -["alice", "bob", "ruth"] -``` +`ruth` differs from other agents in her name and in that grant. You create her +like any other agent, from the swarm UI or `swarmctl agent create`; the hive +creates nothing at boot. Her permissions, prompt and tools work as for every +agent, and destroying her is as final as for any other. -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` permission'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` permission gets bind-mounted (each other -agent's `state` dir, read-write, at `/agents//state`; never `config` or -`harness`). An agent holding no permission sees its own dirs and nothing else: -`ManageRootAgent` is the only grant that reaches another agent's state -dir. See -[`persistence.md`](../../4-implementation/agent-state/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 permission 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. The live set is the - hive's existing agent containers plus the agent that spawns now; reconcile - drops an agent whose config repo exists but whose container doesn't. - -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 that -parses. It refuses to write over a file it can't parse and fails -instead, so a broken hand edit stays until someone fixes it. - -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 -and its socket flavour; its permissions, prompt, tool allow-list and -state dirs work as for every other agent. - - - -- **Naming/bootstrap** — the manager's broker recipient name, state-dir - key, and nixos-container name are all `ruth` (container `h-ruth`). - Like every agent it comes from the swarm dashboard or - `swarmctl agent create`; `hive-c0re` creates nothing at boot. - Roster-wise, `ruth` is just another entry. -- **Wire-protocol** — `hive-core-agent-sock`'s unified `Request` enum - is the same on both socket flavours. Container lifecycle - ops (kill/start/restart/rebuild) go through the separate host-admin - socket `hivectl` speaks. `Wake` (inject a `from: ` 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/4-implementation/turn-loop/mcp.md`](../../4-implementation/turn-loop/mcp.md#waking-the-agent-from-inside-the-container)). -- **Storage/mounts** — the `manage_root_agent` permission, which the swarm - UI's setup assistant creates ruth with, gets a container - each other agent's `state` dir bind-mounted RW at - `/agents//state` (so it can manage any agent's state dir; config - isn't mounted, since a real config change is a PR from a clone), plus an RO mount for - `/meta` (the hive's topology and limits files). Any agent holding it gets the same mounts. -- **Prompt** — identical for every container but one paragraph: an agent - built from an [archetype](../swarm/archetypes.md) is told it has no config - repo, every other agent is told how to propose changes to its own. -- **Tool allow-list** — the MCP tools claude sees come from the agent's - permissions alone, the same mechanism for every agent - ([`docs/4-implementation/turn-loop/mcp.md`](../../4-implementation/turn-loop/mcp.md)). Ruth's permissions - are the default plus `manage_root_agent`, stored for her like any grant. -- **State dirs** — _not_ treated as an exception: `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. - - -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`](../hives/boundary.md) +How the hive reads and rebuilds the file, the wire protocol and the prompt blocks: +[`roster.md`](../../4-implementation/agent-state/roster.md). The operator/agent +trust boundary is a separate axis: [`boundary.md`](../hives/boundary.md). diff --git a/docs/4-implementation/agent-state/roster.md b/docs/4-implementation/agent-state/roster.md new file mode 100644 index 000000000..ba7d05619 --- /dev/null +++ b/docs/4-implementation/agent-state/roster.md @@ -0,0 +1,126 @@ +# Agent roster: topology file and manager behaviour + +The hive-local `topology.json`, how it is derived, and what is special about the +manager (`ruth`). The operator's overview is +[the agent roster page](../../2-handbook/agents/agent-roster.md). + + + +Agents are a **flat set**: the `manage_root_agent` permission scopes +which agents can manage which others. + + + +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/routes/agents.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/` directory's `topology.json`, 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` permission'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` permission gets bind-mounted (each other +agent's `state` dir, read-write, at `/agents//state`; never `config` or +`harness`). An agent holding no permission sees its own dirs and nothing else: +`ManageRootAgent` is the only grant that reaches another agent's state +dir. See +[`persistence.md`](../agent-state/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 permission 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. The live set is the + hive's existing agent containers plus the agent that spawns now; reconcile + drops an agent whose config repo exists but whose container doesn't. + +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 that +parses. It refuses to write over a file it can't parse and fails +instead, so a broken hand edit stays until someone fixes it. + +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 +and its socket flavour; its permissions, prompt, tool allow-list and +state dirs work as for every other agent. + + + +- **Naming/bootstrap** — the manager's broker recipient name, state-dir + key, and nixos-container name are all `ruth` (container `h-ruth`). + Like every agent it comes from the swarm dashboard or + `swarmctl agent create`; `hive-c0re` creates nothing at boot. + Roster-wise, `ruth` is just another entry. +- **Wire-protocol** — `hive-core-agent-sock`'s unified `Request` enum + is the same on both socket flavours. Container lifecycle + ops (kill/start/restart/rebuild) go through the separate host-admin + socket `hivectl` speaks. `Wake` (inject a `from: ` 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/4-implementation/turn-loop/mcp.md`](../turn-loop/mcp.md#waking-the-agent-from-inside-the-container)). +- **Storage/mounts** — the `manage_root_agent` permission, which the swarm + UI's setup assistant creates ruth with, gets a container + each other agent's `state` dir bind-mounted RW at + `/agents//state` (so it can manage any agent's state dir; config + isn't mounted, since a real config change is a PR from a clone), plus an RO mount for + `/meta` (the hive's topology and limits files). Any agent holding it gets the same mounts. +- **Prompt** — identical for every container but one paragraph: an agent + built from an [archetype](../../2-handbook/swarm/archetypes.md) is told it has no config + repo, every other agent is told how to propose changes to its own. +- **Tool allow-list** — the MCP tools claude sees come from the agent's + permissions alone, the same mechanism for every agent + ([`docs/4-implementation/turn-loop/mcp.md`](../turn-loop/mcp.md)). Ruth's permissions + are the default plus `manage_root_agent`, stored for her like any grant. +- **State dirs** — _not_ treated as an exception: `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. + + +None of the above is a stable interface — treat the module doc +comments as the source of truth for exactly which checks exist today. diff --git a/docs/4-implementation/scheduler/coordinator.md b/docs/4-implementation/scheduler/coordinator.md index d44363edd..423c39d48 100644 --- a/docs/4-implementation/scheduler/coordinator.md +++ b/docs/4-implementation/scheduler/coordinator.md @@ -555,4 +555,4 @@ config — all nspawn containers live in `machine.slice`. - `docs/4-implementation/agent-state/config-changes.md` — config PRs, meta flake, system messages - `docs/4-implementation/agent-state/persistence.md` — SQLite schema, state-dir layout - `docs/5-project/process/conventions.md` — wire protocol, recipient sentinels -- `docs/2-handbook/agents/agent-roster.md` — the agent roster and privilege +- `docs/4-implementation/agent-state/roster.md` — the agent roster and privilege diff --git a/docs/README.md b/docs/README.md index 4e00978d5..cbaf1e8d9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -98,7 +98,9 @@ One agent: its roster entry, state, config and page. Start at tables and directory layout: [`4-implementation/agent-state/persistence.md`](4-implementation/agent-state/persistence.md). - **Who can do what to whom — the agent roster and privilege?** → - [`2-handbook/agents/agent-roster.md`](2-handbook/agents/agent-roster.md). + [`2-handbook/agents/agent-roster.md`](2-handbook/agents/agent-roster.md); how + `topology.json` is rebuilt and what is special about the manager: + [`4-implementation/agent-state/roster.md`](4-implementation/agent-state/roster.md). - **What does an agent's own page show?** → [`2-handbook/agents/web-page.md`](2-handbook/agents/web-page.md); the full reference is [`agent-page`](4-implementation/web-ui/agent-page.md).