diff --git a/docs/README.md b/docs/README.md index 163fcd63..b20b3458 100644 --- a/docs/README.md +++ b/docs/README.md @@ -30,7 +30,7 @@ declarations. state machine, `flake.lock` validation). - **What state survives destroy / purge / restart?** → [`agent-lifecycle/persistence.md`](agent-lifecycle/persistence.md). -- **Who can do what to whom — agent hierarchy and privilege?** → +- **Who can do what to whom — the agent roster and privilege?** → [`agent-lifecycle/agent-hierarchy.md`](agent-lifecycle/agent-hierarchy.md). - **How does claude get its prompt, and what tools does it have?** → [`turn-loop/`](turn-loop/README.md) — the loop, binary shape, turn diff --git a/docs/agent-lifecycle/agent-hierarchy.md b/docs/agent-lifecycle/agent-hierarchy.md index a84b235c..69b0dcec 100644 --- a/docs/agent-lifecycle/agent-hierarchy.md +++ b/docs/agent-lifecycle/agent-hierarchy.md @@ -1,109 +1,97 @@ -# Agent hierarchy & privileges +# Agent roster & privileges -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. -## 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 set-parent --parent ` (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 }`. +### 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 | +| -------------------------------------------- | --------------------------------------------------- | +| `` recipient sentinel | address `operator` directly | +| `` fan-out recipient | nothing — name the recipients, or broadcast to `*` | +| `hivectl agent 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 | + -"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. + + ## 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: ` 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: ` 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//` - 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) diff --git a/docs/agent-lifecycle/approvals.md b/docs/agent-lifecycle/approvals.md index bfdcca48..1b635c09 100644 --- a/docs/agent-lifecycle/approvals.md +++ b/docs/agent-lifecycle/approvals.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 diff --git a/docs/agent-lifecycle/persistence.md b/docs/agent-lifecycle/persistence.md index e908efdd..5a558d74 100644 --- a/docs/agent-lifecycle/persistence.md +++ b/docs/agent-lifecycle/persistence.md @@ -335,24 +335,29 @@ Under `/var/lib/hyperhive/agents//`: - `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. -**`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. ⚠️ 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. -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//` — the hive-c0re-only applied repo. Tracks `flake.nix` (module-only boilerplate; never @@ -387,13 +392,11 @@ Contents: `nixosConfigurations.` 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 `` / `` 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 diff --git a/docs/getting-started/setup.md b/docs/getting-started/setup.md index ea23be35..af3290d3 100644 --- a/docs/getting-started/setup.md +++ b/docs/getting-started/setup.md @@ -270,7 +270,7 @@ See [`approvals.md`](../agent-lifecycle/approvals.md) for the full flow. ### 8 · Useful host commands ```bash -# Roster: all agents, status, rev, parent, pending reminders +# Roster: all agents, status, rev, pending reminders hivectl list-agents # Restart a stuck container (no rebuild) diff --git a/docs/process/conventions.md b/docs/process/conventions.md index fbd8e565..ebc221b3 100644 --- a/docs/process/conventions.md +++ b/docs/process/conventions.md @@ -87,24 +87,13 @@ angle-bracket and asterisk shapes below are structurally safe. (`socket_server::handle_send` fans out via `Coordinator::broadcast_send`). - `operator` — the human at the dashboard. Messages accumulate in the inbox view; no agent ever `recv`'s them. -- `` — the sender's parent per `topology.json`. Rewritten at - send time by `topology::resolve_recipient`: looks up - `parent_of(sender)` and falls back to `operator` when the sender is - a root agent (or absent from topology entirely). Lets agents address - their parent without learning the label, so runtime reparenting - propagates with zero agent-side restart. -- `` — fan-out to every direct descendant of the sender per - `topology.json`. Resolved in `socket_server::handle_send` via - `topology::children_of(sender)`: it delivers one message to each - child, bypassing the allow-list check (structural fan-out targets are - never user-listed peers). No-op for leaf agents (returns `Ok` when the - child set is empty). Lets a sub-manager nudge its subtree without - enumerating labels. -When a `` or `` send resolves to real recipients, the -broker stores the *resolved* labels as the message recipients — the -dashboard and recv side see the real routes. The sentinels are purely -send-time addressing conveniences. +`` and `` were two more, resolved against a +`topology.json` parent field. #4472 removed that field and both +sentinels with it: address `operator` where you would have said +``, and name the recipients (or broadcast to `*`) where you +would have said ``. Nothing rewrites a recipient at send time +any more — what an agent passes is what the broker stores. ## Wire protocol diff --git a/docs/scheduler/coordinator.md b/docs/scheduler/coordinator.md index 34fcf982..b5265d10 100644 --- a/docs/scheduler/coordinator.md +++ b/docs/scheduler/coordinator.md @@ -55,37 +55,36 @@ Cheap — no build slot: -| Node | Behavior | -| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `MergeVerify` | the deploy's pre-merge gate — PR-head drift check, fetch, `verify_commit` eval. Mutates nothing, so a rejection here needs no compensation | -| `DeployTail` | the deploy's `AfterAny` compensation + bookkeeping tail: (1) rolls `applied/main` back from the parked `refs/hyperhive/rollback/` and aborts the staged meta lock when the deploy never confirmed good; (2) mirrors whichever deploy tag landed to the forge config repo, always, best-effort; (3) posts the failing build log back onto the config PR when the deploy failed. Named for (2)/(3), which run on the success path too — not `AbortDeploy`. Infallible by construction | -| `MetaSync` | the rebuild's meta preamble — rebuild-dir prep, idempotent meta `sync_agents`, optional per-agent relock. Holds the `MetaWindow` resource (below); deliberately its own node so the window never covers `Prebuild`'s multi-minute build | -| `Provision` | first-spawn pre-create provisioning — proposed/applied repos, state subvolume, meta registration (`sync_agents`); runs ahead of `Create` so the `nixos-container create --flake meta#` ref resolves. Store/meta-only, no container yet | -| `Reconcile` | idempotent power converge: read `wanted` (below) + observed state; start if `Up` & down (cold-start fallback included), stop if `Offline` & up, else noop | -| `Start` | mechanical container start — runtime dir + drop-ins, `start_with_fallback`, MCP listener registration, the manager kick. Fanned out by a `Reconcile` that observed `wanted = Up` and the container down | -| `Stop` | mechanical container stop — `nixos-container` kill, MCP listener unregister, the `Killed` manager notify. Fanned out by a `Reconcile` that observed `wanted = Offline` and up | -| `StopForUpdate` | mechanical `nixos-container stop` for the profile swap; never touches `wanted`; noop if already stopped | -| `RebuildBookkeeping` | the swap's Ok-only bookkeeping tail — rev marker, forge/matrix sync, manager kick, rescan, meta-inputs snapshot; `AfterOk(Swap)` so it runs only on a successful swap (the DAG's `EmitRebuilt` tail node emits the `Rebuilt` manager event, not here). Split out of `Swap` for dashboard visibility + retry granularity, declares no resources of its own — a coordinated child of the `AgentWindow` brace | -| `AgentWindow` | pure resource holder — the brace for one agent's rebuild. Declares the build slot + agent lease atomically and holds both for its whole subtree, so `Prebuild` and the `Signal`→`Drain` quiesce window run concurrently instead of one nested under the other. Performs no work; see _Braces_ | -| `Signal` | set the graceful fence + kick, so the harness runs one stop-checkpoint turn | -| `Drain` | await the harness clearing the fence, bounded by the 3-min graceful-stop timeout; resolves ok either way | -| `PauseSignal` | write the pause marker + mark `pause_pending`. No kick, unlike `Signal` — the harness's between-turns poll is already responsive enough, and `Signal`'s kick-message body ("you were just (re)started") would be actively misleading here | -| `PauseDrain` | await the harness reporting `PauseAcknowledged`, bounded timeout; best-effort like `Drain` | -| `DestroyContainer` | `nixos-container destroy` + un-registration (drop from the roster, clear the ephemeral runtime dir). Runs downstream of a `Stop`, so deliberately excluded from `takes_container_down` — the container is already down by the time it claims | -| `PurgeState` | the `purge = true` half of a destroy: delete the agent's state subvolume (via hive-priv) plus its state/applied dirs. Own node because it's conditional and the irreversible step | -| `DestroyBookkeeping` | the post-destroy tail — meta sync, fail pending approvals, drop the power intent, notify the manager, rescan, re-emit the tombstone, resync tmpfiles. Same split rationale as `RebuildBookkeeping`/`Swap`. Its `purge` flag only selects the wording of the approval-failure reason and the manager notification — the destructive work is `PurgeState`'s | -| `SetWanted` | write the durable power intent (`wanted = Up`/`Offline`) as the head node of a power-op DAG, replacing the old pre-submit side effect. Takes the agent lease even though it's a store write, so the intent write and the tail `Reconcile` are atomic per-agent — two racing power ops can't clobber each other's intent before either reconciles | -| `FinalizeDeploy` | deploy phase 3 — drop the rollback ref, plant `deployed/`, commit the staged `flake.lock`. The first two git steps are fatal on purpose, so a confirmed-good deploy's outcome and the repo's state can't disagree | -| `ResolveApproval` | tail of an approval-carrying DAG — resolve the approval row from how the work ended (`AfterAny`, one node emitted per outcome). Agentless: the approval row already names its agent | -| `EmitRebuilt` | tail of a rebuild/perm-change — emit the agent's `Rebuilt` manager event (ok/fail per outcome, nothing on cancel). One node per agent _and_ per outcome | -| `WriteDropin` | `set_nspawn_flags` + `set_resource_limits` + daemon-reload | -| `WritePermFile` | commit `tool-groups.json` / `capabilities.json` (single git commit under `META_LOCK`) + emit the P3RM1SS10NS snapshots | -| `Reparent` | `set-parent` / `set-parent-bulk`: apply every `(child, new_parent)` move under one `META_LOCK` commit (`meta::bulk_commit_topology`), send the per-agent move notifications, rescan + diff-emit. Agentless like `MetaLock` — a bulk move can span multiple agents, and a reparent touches the meta repo, not any one container. `moves` is typed `(Ident, Option)` pairs, not raw strings. Rides the existing `Template::MetaUpdate` variant rather than a dedicated one — it's internal-only (never reaches the graph wire), so the stand-in only affects `terminal_hook` dispatch (resolves to no hook either way) and history-retention bucketing | -| `ForgeSweep` | one-shot boot-time forge user/token sweep for every container (`forge::ensure_all`) as a first-class node, so it shows as real work on the dashboard instead of running invisibly in a bare `tokio::spawn`. Agentless | -| `MatrixSweep` | same as `ForgeSweep`, for matrix (`matrix::ensure_all`). The periodic 30-min re-sweep stays a background loop in `main.rs`; only the boot-time instance is a node | -| `WebhookRegister` | one-shot boot-time Forgejo webhook registration (`internal/knowledge` push→pull, `agent-configs` PR→approval). No-op until the core token, hive domain, and HMAC secret are all available. Agentless | -| `KnowledgePull` | one-shot boot-time `/knowledge` pull (`knowledge::pull`), reconciling commits that landed while `hive-c0re` was down. Same rationale as `MatrixSweep`: the periodic hourly re-pull stays a background loop | -| `WantedPull` | one-shot boot-time pull of the agent set the swarm controller declares for this hive (`wanted::pull`), converging the agents it names. No background loop behind this one — boot is the whole cadence; the deploy event (`swarm_status`) is the fast path, this repairs a missed one. Agentless | +| Node | Behavior | +| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `MergeVerify` | the deploy's pre-merge gate — PR-head drift check, fetch, `verify_commit` eval. Mutates nothing, so a rejection here needs no compensation | +| `DeployTail` | the deploy's `AfterAny` compensation + bookkeeping tail: (1) rolls `applied/main` back from the parked `refs/hyperhive/rollback/` and aborts the staged meta lock when the deploy never confirmed good; (2) mirrors whichever deploy tag landed to the forge config repo, always, best-effort; (3) posts the failing build log back onto the config PR when the deploy failed. Named for (2)/(3), which run on the success path too — not `AbortDeploy`. Infallible by construction | +| `MetaSync` | the rebuild's meta preamble — rebuild-dir prep, idempotent meta `sync_agents`, optional per-agent relock. Holds the `MetaWindow` resource (below); deliberately its own node so the window never covers `Prebuild`'s multi-minute build | +| `Provision` | first-spawn pre-create provisioning — proposed/applied repos, state subvolume, meta registration (`sync_agents`); runs ahead of `Create` so the `nixos-container create --flake meta#` ref resolves. Store/meta-only, no container yet | +| `Reconcile` | idempotent power converge: read `wanted` (below) + observed state; start if `Up` & down (cold-start fallback included), stop if `Offline` & up, else noop | +| `Start` | mechanical container start — runtime dir + drop-ins, `start_with_fallback`, MCP listener registration, the manager kick. Fanned out by a `Reconcile` that observed `wanted = Up` and the container down | +| `Stop` | mechanical container stop — `nixos-container` kill, MCP listener unregister, the `Killed` manager notify. Fanned out by a `Reconcile` that observed `wanted = Offline` and up | +| `StopForUpdate` | mechanical `nixos-container stop` for the profile swap; never touches `wanted`; noop if already stopped | +| `RebuildBookkeeping` | the swap's Ok-only bookkeeping tail — rev marker, forge/matrix sync, manager kick, rescan, meta-inputs snapshot; `AfterOk(Swap)` so it runs only on a successful swap (the DAG's `EmitRebuilt` tail node emits the `Rebuilt` manager event, not here). Split out of `Swap` for dashboard visibility + retry granularity, declares no resources of its own — a coordinated child of the `AgentWindow` brace | +| `AgentWindow` | pure resource holder — the brace for one agent's rebuild. Declares the build slot + agent lease atomically and holds both for its whole subtree, so `Prebuild` and the `Signal`→`Drain` quiesce window run concurrently instead of one nested under the other. Performs no work; see _Braces_ | +| `Signal` | set the graceful fence + kick, so the harness runs one stop-checkpoint turn | +| `Drain` | await the harness clearing the fence, bounded by the 3-min graceful-stop timeout; resolves ok either way | +| `PauseSignal` | write the pause marker + mark `pause_pending`. No kick, unlike `Signal` — the harness's between-turns poll is already responsive enough, and `Signal`'s kick-message body ("you were just (re)started") would be actively misleading here | +| `PauseDrain` | await the harness reporting `PauseAcknowledged`, bounded timeout; best-effort like `Drain` | +| `DestroyContainer` | `nixos-container destroy` + un-registration (drop from the roster, clear the ephemeral runtime dir). Runs downstream of a `Stop`, so deliberately excluded from `takes_container_down` — the container is already down by the time it claims | +| `PurgeState` | the `purge = true` half of a destroy: delete the agent's state subvolume (via hive-priv) plus its state/applied dirs. Own node because it's conditional and the irreversible step | +| `DestroyBookkeeping` | the post-destroy tail — meta sync, fail pending approvals, drop the power intent, notify the manager, rescan, re-emit the tombstone, resync tmpfiles. Same split rationale as `RebuildBookkeeping`/`Swap`. Its `purge` flag only selects the wording of the approval-failure reason and the manager notification — the destructive work is `PurgeState`'s | +| `SetWanted` | write the durable power intent (`wanted = Up`/`Offline`) as the head node of a power-op DAG, replacing the old pre-submit side effect. Takes the agent lease even though it's a store write, so the intent write and the tail `Reconcile` are atomic per-agent — two racing power ops can't clobber each other's intent before either reconciles | +| `FinalizeDeploy` | deploy phase 3 — drop the rollback ref, plant `deployed/`, commit the staged `flake.lock`. The first two git steps are fatal on purpose, so a confirmed-good deploy's outcome and the repo's state can't disagree | +| `ResolveApproval` | tail of an approval-carrying DAG — resolve the approval row from how the work ended (`AfterAny`, one node emitted per outcome). Agentless: the approval row already names its agent | +| `EmitRebuilt` | tail of a rebuild/perm-change — emit the agent's `Rebuilt` manager event (ok/fail per outcome, nothing on cancel). One node per agent _and_ per outcome | +| `WriteDropin` | `set_nspawn_flags` + `set_resource_limits` + daemon-reload | +| `WritePermFile` | commit `tool-groups.json` / `capabilities.json` (single git commit under `META_LOCK`) + emit the P3RM1SS10NS snapshots | +| `ForgeSweep` | one-shot boot-time forge user/token sweep for every container (`forge::ensure_all`) as a first-class node, so it shows as real work on the dashboard instead of running invisibly in a bare `tokio::spawn`. Agentless | +| `MatrixSweep` | same as `ForgeSweep`, for matrix (`matrix::ensure_all`). The periodic 30-min re-sweep stays a background loop in `main.rs`; only the boot-time instance is a node | +| `WebhookRegister` | one-shot boot-time Forgejo webhook registration (`internal/knowledge` push→pull, `agent-configs` PR→approval). No-op until the core token, hive domain, and HMAC secret are all available. Agentless | +| `KnowledgePull` | one-shot boot-time `/knowledge` pull (`knowledge::pull`), reconciling commits that landed while `hive-c0re` was down. Same rationale as `MatrixSweep`: the periodic hourly re-pull stays a background loop | +| `WantedPull` | one-shot boot-time pull of the agent set the swarm controller declares for this hive (`wanted::pull`), converging the agents it names. No background loop behind this one — boot is the whole cadence; the deploy event (`swarm_status`) is the fast path, this repairs a missed one. Agentless | @@ -100,7 +99,7 @@ container build: - **The deploy window** (`Resource::MetaWindow`): a global, capacity-1 queue resource declared by every node kind that mutates the meta repo — `MetaSync`, - `MetaLock`, `WritePermFile`, `Reparent`, `Provision`'s agent registration, and + `MetaLock`, `WritePermFile`, `Provision`'s agent registration, and `DeployWindow` — the deploy subtree's root, which holds it across every phase below it (it declares `Resource::MetaWindow`). Two meta mutations can therefore never interleave, so no commit lands inside another @@ -156,7 +155,6 @@ perm-change(a): WritePermFile(a) → «rebuild subgraph» meta-update(inp): MetaLock(inp) →(in-DAG) «rebuild subgraph» per affected agent boot: (if any rev marker stale) MetaLock(hyperhive) →(in-DAG) «rebuild subgraph» per stale agent; plus Reconcile(a) for every drifted agent (all ONE DAG) -reparent(moves): Reparent(moves) (no rebuild — topology.json is read live) ``` Notable collapses: @@ -229,7 +227,7 @@ resources are free. Resources: (`SetWanted` is a store write, not a container op, but takes the lease anyway so a power-op DAG's intent write + reconcile is atomic — two racing ops can't clobber intent before either reconciles.) **Lease-exempt**: `MetaSync`, - `Prebuild`, `Provision`, `MetaLock`, `WritePermFile`, `Reparent` — they + `Prebuild`, `Provision`, `MetaLock`, `WritePermFile` — they touch the store / meta, not the running container, which is exactly why a stop can land while another DAG's prebuild is still building. Also exempt, for a different reason, are the rebuild subtree's own members (`StopForUpdate`, `Swap`, @@ -374,8 +372,7 @@ vs the current flake path) and persisted `wanted` intent, then: 1. **Config path** — when _any_ marker is stale, submit one `Boot` DAG: a `MetaLock` (hyperhive input bump, non-fatal) that grows an in-DAG - `Rebuild` subgraph for each stale agent whose `wanted = Up` (topology-sorted, - parents first). Stale but wanted-offline agents get no boot-time nix work — their + `Rebuild` subgraph for each stale agent whose `wanted = Up` (name-sorted). Stale but wanted-offline agents get no boot-time nix work — their rebuild happens on their next start (the start submit path upgrades a stale start to rebuild+start), which is also why the lock bump runs even when every stale agent is offline: those later start-upgrades must build @@ -584,4 +581,4 @@ which is almost never desired. Leave off on non-x86 hosts. - `docs/agent-lifecycle/approvals.md` — approval flow + scheduled prompts - `docs/agent-lifecycle/persistence.md` — SQLite schema, state-dir layout - `docs/process/conventions.md` — wire protocol, recipient sentinels -- `docs/agent-lifecycle/agent-hierarchy.md` — topology and parent/child relations +- `docs/agent-lifecycle/agent-hierarchy.md` — the agent roster and privilege diff --git a/docs/tools/hivectl-cli.md b/docs/tools/hivectl-cli.md index 38cd6602..18881495 100644 --- a/docs/tools/hivectl-cli.md +++ b/docs/tools/hivectl-cli.md @@ -31,7 +31,6 @@ This document contains the help content for the `hivectl` command-line program. * [`hivectl agent kill`↴](#hivectl-agent-kill) * [`hivectl agent destroy`↴](#hivectl-agent-destroy) * [`hivectl agent rebuild`↴](#hivectl-agent-rebuild) -* [`hivectl agent set-parent`↴](#hivectl-agent-set-parent) * [`hivectl agent set-limits`↴](#hivectl-agent-set-limits) * [`hivectl agent choom`↴](#hivectl-agent-choom) * [`hivectl agent watch`↴](#hivectl-agent-watch) @@ -351,7 +350,6 @@ Everything here targets a single named agent (`hivectl agent foo restart`, `hive * `kill` — Hard-stop this managed container * `destroy` — Tear down this sub-agent container, keeping its state by default. No undo * `rebuild` — Apply pending config to this managed container -* `set-parent` — Move this agent in the topology tree — under a new parent, or to root * `set-limits` — Declare this agent's CPU/memory limits, overriding the hive-wide defaults * `choom` — Open an interactive Claude session inside this agent's container * `watch` — Follow this agent's live turn/tool-call event stream from the CLI @@ -464,19 +462,6 @@ Apply pending config to this managed container -## `hivectl agent set-parent` - -Move this agent in the topology tree — under a new parent, or to root - -**Usage:** `hivectl agent set-parent [OPTIONS]` - -###### **Options:** - -* `--parent ` — New parent agent name. Mutually exclusive with `--root` -* `--root` — Promote this agent to root (no parent) - - - ## `hivectl agent set-limits` Declare this agent's CPU/memory limits, overriding the hive-wide defaults. diff --git a/docs/tools/hivectl.md b/docs/tools/hivectl.md index 5a40ddb0..feee22a6 100644 --- a/docs/tools/hivectl.md +++ b/docs/tools/hivectl.md @@ -153,12 +153,11 @@ hivectl agent iris resume # let it drive turns again, draining what que ``` `list-agents` prints a padded table with one row per managed agent — -`NAME STATUS REV PARENT REMIND`. STATUS collapses the health flags +`NAME STATUS REV REMIND`. STATUS collapses the health flags (`running` / `stopped`, plus ` paused` / ` needs-login` / ` needs-update` when set — `paused` is orthogonal to running, see below); -REV is the first 12 chars of the agent's locked config sha; PARENT is its -place in the topology tree (`-` for a root agent); REMIND is the count of -pending reminders. It reuses the same per-agent aggregation the dashboard +REV is the first 12 chars of the agent's locked config sha; REMIND is the +count of pending reminders. It reuses the same per-agent aggregation the dashboard renders, so the CLI roster and the web UI never drift. `--json` emits the raw rows instead of the table. diff --git a/docs/turn-loop/README.md b/docs/turn-loop/README.md index 6ef2233e..03fa27ac 100644 --- a/docs/turn-loop/README.md +++ b/docs/turn-loop/README.md @@ -111,9 +111,7 @@ merge work. Plugin install failures aren't fatal: each entry comes back as a human-readable failure string that gets routed via -`Surface::send_to_parent` to the agent's topology parent (the -broker resolves `` per `topology::resolve_recipient`; root -agents and the manager fall through to operator). +`Surface::send_to_operator` to the operator. ### Turn outcomes @@ -128,7 +126,7 @@ else a `TurnError`) drives the post-claude branch: | `Err(AuthFailed)` | emit `needs_login_idle` sentinel, requeue inflight, park in `wait_for_login` | | `Err(SessionNotFound)` | resume + create self-heal both missed ("shouldn't happen"); requeue inflight so the next turn creates fresh — no status park, message not dropped | | `Err(ApiStall)` | idle watchdog killed claude after `HIVE_TURN_IDLE_SECS` (default 600) of output silence; sleep `HIVE_STALL_SLEEP_SECS` (default 60), requeue inflight, status back to `online` | -| `Err(Failed(err))` | route `[system] \`\` claude turn failed:\n` to `` via `send_to_parent` | +| `Err(Failed(err))` | route `[system] \`\` claude turn failed:\n` to `operator` via `send_to_operator` | `ApiStall` catches an Anthropic API stall — a multi-retry connection storm where the stream goes silent for minutes. The idle watchdog lives in `hive-claude`'s diff --git a/docs/turn-loop/mcp.md b/docs/turn-loop/mcp.md index 22b0a00a..c54589c7 100644 --- a/docs/turn-loop/mcp.md +++ b/docs/turn-loop/mcp.md @@ -29,15 +29,13 @@ are opt-in via the P3RM1SS10NS tab. `recv(max?)`, `ack_until(up_to)`. - `send` — message a peer (logical name) or the operator - (`to: "operator"`). Use `to: ""` to address the topology - parent without hardcoding the label; the broker resolves the - sentinel at delivery time. Optional `in_reply_to: i64` links the + (`to: "operator"`). Optional `in_reply_to: i64` links the message to a prior id for thread rendering. Per-agent `services.hyperhive.agent.allowedRecipients` (default: empty = unrestricted) limits which names `send` accepts — useful for sandboxing: set - `[ "operator" ]` to restrict a sub-agent to operator messages only - (the topology parent is always reachable regardless of this list — - that carve-out is structural, keyed on parent relationship, not name). + `[ "operator" ]` to restrict a sub-agent to operator messages only. + The operator stays reachable regardless of this list, so an agent can + always report a block. - `recv` — drain inbox. Always an immediate peek, never blocks. `max` (default 1, cap 5) drains up to N rows. `recv` prefixes each returned row with `[msg #]` (broker row id; note the highest id seen, then pass diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index 488117b8..9621ce58 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -56,16 +56,12 @@ surfaces, not tab panes. ## SW4RM tab -**C0NTAINERS** — live containers rendered as a depth-first -tree using `ContainerView.parent` (populated by -`hive-c0re/src/agent_config/topology.rs` — don't confuse it with -`hive-c0re/src/dashboard/topology.rs`, which only holds the -set-parent endpoints). -ASCII tree glyphs (`├─`, `└─`, `│ ` continuation columns) prefix -each container's row, showing the agent parent/child hierarchy. -When every container has `parent = null` -(flat topology) the tree collapses to a plain list with no -glyphs. The tree sorts children alphabetically within each parent; +**C0NTAINERS** — live containers rendered as a flat, +alphabetically-sorted list. The renderer still walks +`ContainerView.parent` for a depth-first tree with ASCII glyphs +(`├─`, `└─`, `│ ` continuation columns), but #4472 removed that field, +so every row sits at depth 0 and the renderer paints no glyph. The tree +sorts alphabetically; roots likewise. The tree tolerates cycles in the parent graph — it appends orphaned containers (not reachable from any root) as roots so no agent disappears. Pulsing red banner at the top @@ -961,7 +957,7 @@ Three primitives, all built on the `openDialog` core: summaries are sticky (select to dismiss) so they aren't missed. Every destructive run-state action (`ST0P`, `R3ST4RT`, `R3BU1LD`, -`DESTR0Y`, `PURG3`, `M0V3`) routes through `themedConfirm`, on both +`DESTR0Y`, `PURG3`) routes through `themedConfirm`, on both the per-agent `⋮` menu and the bulk selection bar. **Graceful stop** — the `■ ST0P` confirm dialog (per-agent and @@ -985,10 +981,12 @@ identically.) ### Topology tree -See **SW4RM tab** above for the parent/child derivation, sibling -sort order, and cycle-safety rules (`swarm.js::buildAgentTree` walks -`ContainerView.parent`) — this section covers only how the tree is -*drawn*. +⚠️ **Dormant since #4472,** which removed `ContainerView.parent`: +`buildAgentTree` now puts every container at depth 0 and the rules below are +all no-ops — the list renders flat. The walk and the prefix painting are +still in `swarm.js`; retiring them falls to the frontend owner rather +than to this removal. What follows describes what the code +still does, for whoever makes that call. The per-row prefix column (`.tree-prefix`) is **DOM-painted, not text-glyph-painted**: each indent lane is its own positioned `` @@ -1002,8 +1000,8 @@ ancestor's still-open subtree, and the joint at a row's own depth is the row's icon midline). Exact lane widths and positioning live in `swarm.js`'s tree-prefix rendering and its paired CSS rules — not reproduced here since they're tuned in pixel units and will drift. -When every container is at depth 0 (no `parent` set) these rules are -all no-ops and the layout reads like a plain flat list. +With every container at depth 0 these rules are all no-ops and the +layout reads like a plain flat list, which is what a hive renders today. ## Selection bar @@ -1032,25 +1030,10 @@ frosted-mauve bar slides up from the bottom of the viewport - `▶ R3SUM3` — paused agents only - `↻ R3BU1LD` — always available - `DESTR0Y` / `PURG3` — always available - - `⇡ M0V3 → ROOT` — promote selected agents to top-level - (parent = null); disabled when all selected are already at root. - hive-c0re's `topology::set_parent` refuses moves it can't satisfy - (for example a move that would create a cycle) and the refusal surfaces - in the failure roll-up. - - `⇢ M0V3 → [select]` — inline picker available for any - selection size. The dropdown lists every container that isn't IN - the selection itself nor a descendant of any selected agent - (client-side BFS cycle prevention across the whole batch; the - hive-c0re re-checks per-agent). On submit: - - **single agent** → `POST /api/topology/set-parent` - (form-encoded `child=&new_parent=`) - - **multiple agents** → `POST /api/topology/set-parent-bulk` - (JSON `[{ child, new_parent }]`; all moves land in a single - `topology.json` commit instead of one per agent) - Both write `topology.json` and re-emit a container snapshot so - the tree repaints without a page reload. + #4472 removed the `M0V3` picker that used to sit here — an agent has + no parent to move it to any more. - **`✕ clear`** button + `Esc` key clear the entire selection. Every render prunes stale selections (agents destroyed while diff --git a/frontend/packages/dashboard/src/swarm.js b/frontend/packages/dashboard/src/swarm.js index 107b233d..69c77bab 100644 --- a/frontend/packages/dashboard/src/swarm.js +++ b/frontend/packages/dashboard/src/swarm.js @@ -952,7 +952,6 @@ export function renderSelectionBar(containers) { confirm: (names) => `PURGE ${names.length} agent${names.length === 1 ? "" : "s"} (${names.join(", ")})? containers, config history, claude creds, and notes are all WIPED. no undo.`, }); - } function addBulkButton(parent, btnClass, label, enabled, selected, opts) { diff --git a/hive-c0re/src/agent_config/topology.rs b/hive-c0re/src/agent_config/topology.rs index 3acd1ccd..60ba5a93 100644 --- a/hive-c0re/src/agent_config/topology.rs +++ b/hive-c0re/src/agent_config/topology.rs @@ -192,7 +192,10 @@ mod tests { #[test] fn all_agents_in_returns_every_name_sorted() { - assert_eq!(all_agents_in(&roster_three()), vec!["alice", "bob", "carol"]); + assert_eq!( + all_agents_in(&roster_three()), + vec!["alice", "bob", "carol"] + ); } #[test] diff --git a/nix/agent-modules/agent-service.nix b/nix/agent-modules/agent-service.nix index b806ac33..bfa2259f 100644 --- a/nix/agent-modules/agent-service.nix +++ b/nix/agent-modules/agent-service.nix @@ -151,9 +151,9 @@ in Place the file inside the agent's bind-mounted **harness** dir (e.g. `/agents//harness/openrouter.env`, `$HYPERHIVE_HARNESS_DIR`), not `state/` — `harness/` survives container rebuilds exactly like - `state/` does, but is never bind-mounted into a parent agent's - container (unlike `state/`, which a parent gets read-write for child - recovery — see `docs/agent-lifecycle/persistence.md`'s "Parent access to child + `state/` does, but is never bind-mounted into another agent's + container (unlike `state/`, which a `ManageRootAgent` holder gets + read-write for recovery — see `docs/agent-lifecycle/persistence.md`'s "Cross-agent access to state"), so this credential is reachable by nothing but this agent and the host. Permissions should be `0600`, owned by the agent's unix user. Loaded with a leading `-` (optional `EnvironmentFile`), diff --git a/nix/agent-modules/user.nix b/nix/agent-modules/user.nix index 19f66a11..52424e00 100644 --- a/nix/agent-modules/user.nix +++ b/nix/agent-modules/user.nix @@ -183,20 +183,21 @@ in mkdir -p "$(dirname "$marker")" : > "$marker" # Scope state + harness chowns to THIS container's own dirs only. - # The glob `/agents/*/state` also matches child-agent state dirs that - # are bind-mounted into parent containers, which would clobber the - # ownership those dirs' own activation scripts set — producing - # intermittent EACCES for the child agent's harness between a parent - # rebuild and the child's next activation. Config dirs are kept broad - # because the parent legitimately owns child proposed-config repos. + # The glob `/agents/*/state` also matches other agents' state dirs + # bind-mounted into a `ManageRootAgent` holder's container, which + # would clobber the ownership those dirs' own activation scripts set + # — producing intermittent EACCES for that agent's harness between + # the holder's rebuild and its own next activation. Config dirs are + # kept broad because the holder legitimately owns the proposed-config + # repos it edits. if [ -d "/agents/$userName/state" ]; then chown -hR "$userName:$userName" "/agents/$userName/state" 2>/dev/null || true fi if [ -d "/agents/$userName/harness" ]; then chown -hR "$userName:$userName" "/agents/$userName/harness" 2>/dev/null || true fi - # The proposed-config repo is RW-mounted into the editing (parent/ - # manager) agent and owned by it; hive-c0re only pulls from it. Heal + # The proposed-config repo is RW-mounted into the editing agent and + # owned by it; hive-c0re only pulls from it. Heal # it to this user too — same as state/harness. In an agent's own # container its config is RO-mounted, so the chown there just fails # harmlessly (|| true). diff --git a/nix/reserved-names.nix b/nix/reserved-names.nix index 95599b28..bfc26857 100644 --- a/nix/reserved-names.nix +++ b/nix/reserved-names.nix @@ -60,11 +60,7 @@ # against hive names, in `swarm-otel.nix`'s own `reservedOwners`. "swarm" ] -# Deliberately absent, and both are load-bearing omissions: -# -# `` / `` — routing recipients the ident charset already -# rejects, so no name can ever equal them; listing them would imply a guard -# that never fires. +# Deliberately absent, and it is a load-bearing omission: # # `ruth` — a real agent, not a literal. A second agent wanting that name is a # name that is TAKEN, which the roster answers, not this list.