docs: retire the agent hierarchy from every page that described it

The topology doc keeps its filename and its second half (manager
special-casing, harness unit shape) — both are cross-referenced from
other pages and neither is about the parent field. Its first half is
rewritten: what topology.json is now, and a table of what the removal
took with it, so a reader who finds `<parent>` or `set-parent` in an old
issue thread learns it went away rather than moved.

The dashboard's tree-rendering section is marked dormant rather than
deleted: the walk is still in swarm.js and retiring it is the frontend
owner's call.
This commit is contained in:
atlas 2026-09-21 21:18:08 +02:00 committed by atlas
commit 179f873722
17 changed files with 186 additions and 262 deletions

View file

@ -30,7 +30,7 @@ declarations.
state machine, `flake.lock` validation). state machine, `flake.lock` validation).
- **What state survives destroy / purge / restart?** - **What state survives destroy / purge / restart?**
[`agent-lifecycle/persistence.md`](agent-lifecycle/persistence.md). [`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). [`agent-lifecycle/agent-hierarchy.md`](agent-lifecycle/agent-hierarchy.md).
- **How does claude get its prompt, and what tools does it have?** - **How does claude get its prompt, and what tools does it have?**
[`turn-loop/`](turn-loop/README.md) — the loop, binary shape, turn [`turn-loop/`](turn-loop/README.md) — the loop, binary shape, turn

View file

@ -1,109 +1,97 @@
# Agent hierarchy & privileges # Agent roster & privileges
<!-- vale write-good.Passive = NO --> <!-- vale write-good.Passive = NO -->
Every agent has a place in an operator-editable parent/child tree, used Agents are a **flat set**, with no parent/child tree: #4472 removed the
to scope which agents can manage which others. This doc covers how `parent` field `topology.json` used to carry, and every mechanism that
hive-c0re stores and edits the tree today, the rules that are meant to run on top read it. The capability store scopes which agents can manage which
of it once enforcement is finished, and where the manager still gets others; a tree position no longer scopes anything.
special-cased in the meantime, as a tracked cleanup.
<!-- vale write-good.Passive = YES --> <!-- vale write-good.Passive = YES -->
## 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`: `flake.nix`, at `/var/lib/hyperhive/meta/topology.json`:
```json ```json
{ ["alice", "bob", "ruth"]
"ruth": null,
"alice": null,
"bob": "alice"
}
``` ```
`null` = root-level agent. New agents **default to root** — there is no One entry per agent the hive knows about, in name order. The file
structural manager that everything hangs under. The operator builds carries no per-agent value any more, and encodes no ordering or
hierarchy explicitly: an agent gets a parent edge written before its grouping — it answers exactly one question, _which agents exist,_ and
first spawn, or the operator reparents it afterwards (so `bob` above sits under `topology::all_agents` is the only reader that matters.
`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).
### 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 <child> set-parent --parent <new>` (or `--root` ### Reading the pre-#4472 format
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<String> }`.
All three go through the same validation, which refuses: `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
- unknown `child` / `new_parent` (typo guard), the change reads the same roster rather than an empty one. An empty
- self-parenting, roster costs more than a cosmetic gap: every capability holder loses its
- cycles (a bounded ancestor walk — moving the manager under one of mounts until the next reconcile pass writes the array form.
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.
### Why meta, not per-agent `agent.nix` ### Why meta, not per-agent `agent.nix`
An agent shouldn't be able to claim a parent without that parent's An agent shouldn't be able to add itself to a set that governs who may
consent, and operator-driven re-parenting shouldn't require touching reach its state dir. The roster IS a system-level fact; meta is where
the moved agent's config. Topology IS a system-level concern; meta is system-level facts live.
where system-level facts live.
### How `topology.json` gets updated ### How `topology.json` gets updated
- **Read** — parsed into an agent→parent map; a missing or unparsable - **Read** — parsed into a set of names; a missing or unparsable file
file degrades safely to "every agent is root" (covers a fresh degrades safely to "no agents" (covers a fresh install that hasn't
install that hasn't synced yet). synced yet).
- **Reconcile** — runs alongside the periodic meta/flake regeneration. - **Reconcile** — runs alongside the periodic meta/flake regeneration.
New agents default to root unless they already carry an explicit Adds newly-spawned agents, drops removed ones. Reconcile keeps agents
parent edge written before their first spawn; Reconcile preserves whose config repo exists but that haven't spawned yet, so the gap until
existing entries (including operator overrides); removed agents drop. the container appears doesn't churn the file.
Agents whose config repo exists but that haven't spawned yet keep
their edge too, so it survives the gap until the container actually No write API and no operator verb reach this file. Reconcile derives it
appears. from which agents exist, so the next pass overwrites a hand edit.
- **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.
See `hive-c0re/src/agent_config/topology.rs` and `hive-c0re/src/meta.rs`'s See `hive-c0re/src/agent_config/topology.rs` and `hive-c0re/src/meta.rs`'s
module docs for the exact call chain. 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 Recorded so a reader who finds one of these in an old branch, an issue
(`root`) gets `/var/lib/hyperhive/agents` bind-mounted at `/agents` in thread or a stale comment knows each one went away rather than moved:
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.
## Planned topology semantics (once ancestor-based enforcement lands) | 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 |
| operation | who can do it | <!-- vale write-good.Passive = NO -->
| ----------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| 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 The last row is the one with teeth: an agent that used to reach a child's
cycles at dispatch time (a malformed `topology.json` can't lock state dir by virtue of being its parent no longer reaches it at all
the dispatcher into a loop). 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 ## 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** **manager (`ruth`) still gets some hard-coded special treatment**
other agents don't: other agents don't:
@ -113,13 +101,12 @@ other agents don't:
key, and nixos-container name are all `ruth` (container `h-ruth`). key, and nixos-container name are all `ruth` (container `h-ruth`).
`hive-c0re` spawns it directly at boot if missing, with no operator `hive-c0re` spawns it directly at boot if missing, with no operator
approval step — every other agent goes through a `Spawn` approval. 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 - **Wire-protocol** — the privileged `Request` variants
(`Kill` / `Start` / `Restart` / `Update`; `GetLogs`) — marked (`Kill` / `Start` / `Restart` / `Update`; `GetLogs`) — marked
`*(privileged)*` in `hive-core-agent-sock`'s unified `Request` enum — `*(privileged)*` in `hive-core-agent-sock`'s unified `Request` enum —
are reachable only from the manager's socket flavour today. Planned are reachable only from the manager's socket flavour today; each is
rule for each is in the table above ("any ancestor" for planned to become a capability check. One exception: `Wake` (inject a `from: <X>` message into the
lifecycle/logs). One exception: `Wake` (inject a `from: <X>` message into the
caller's own inbox) isn't really privileged — every per-agent daemon 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 (for example `hive-forge-notify`) needs it, and sub-agents already have the
equivalent on their own socket. 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 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 real config change is a PR from a clone), plus RO mounts for
`/applied` (diff against what's deployed) and `/meta` (system-wide `/applied` (diff against what's deployed) and `/meta` (system-wide
deploy log). Planned: each agent gets RW to `/agents/<descendant>/` deploy log). That grant is the `ManageRootAgent` capability now, and
for just its own subtree — the manager's full-forest RW becomes the ruth holds it; no name check remains. hive-c0re will
"root's subtree is everything" case of that same rule. hive-c0re will
gate RO `/meta` access on a "meta read" capability; no agent-facing gate RO `/meta` access on a "meta read" capability; no agent-facing
path writes `flake.lock` any more — `request_update_meta_inputs` was path writes `flake.lock` any more — `request_update_meta_inputs` was
removed, leaving the operator dashboard's `POST 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), visibility (manager sees hive-wide, sub-agents only their own),
`destroy` refusing to act on the manager, and crash-watch skipping `destroy` refusing to act on the manager, and crash-watch skipping
the manager (it autorestarts via systemd instead of going through the manager (it autorestarts via systemd instead of going through
the crash-watch loop). Each is planned to become an the crash-watch loop). Each is planned to become a capability
ancestor/descendant check instead of a manager-name check — see the check instead of a manager-name check — see the
module docs for `loose_ends.rs`, `stores/broker.rs`, `actions.rs`, module docs for `loose_ends.rs`, `stores/broker.rs`, `actions.rs`,
and `workers/crash_watch.rs` for the current owner-check logic in and `workers/crash_watch.rs` for the current owner-check logic in
each. (The harness handles reminder cancellation fully in-agent — see 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 None of the above is a stable interface — treat the module doc
comments as the source of truth for exactly which checks exist today. 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 ## Harness systemd unit shape
One harness serve binary (`hive-agent`, with its `hive-agent-mcp` 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 ## Cross-references
- Milestone: "Agent privileges and sub-agents" (tracked internally) - 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) - Audit table source: milestone comment (tracked internally)
- Operator/agent trust boundary (orthogonal axis): [`boundary.md`](../trust-boundary/boundary.md) - Operator/agent trust boundary (orthogonal axis): [`boundary.md`](../trust-boundary/boundary.md)

View file

@ -483,12 +483,12 @@ approval card. See `docs/web-ui/dashboard.md`.
### Submitting agent's view of config repos ### Submitting agent's view of config repos
Every parent agent's container has its **direct children's** config An agent holding `ManageRootAgent` has every other agent's config repo
repos bind-mounted **read-only** (topology-driven: bind-mounted **read-only** (`hive-c0re/src/lifecycle/host_config.rs`
`hive-c0re/src/lifecycle/host_config.rs` calls `bind_child_agent_dirs` calls `bind_child_agent_dirs` for each entry in
for each entry in `topology::all_agents()`). it's a copy to *read* another agent's
`topology::children_of(agent_name)`). it's a copy to *read* a child's current config — not an editing surface. An agent without the
current config — not an editing surface. capability sees no other agent's config at all.
An agent with the `approvals` tool group submits a change the same way 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 it makes any other change: **clone the child's config repo from the

View file

@ -335,24 +335,29 @@ Under `/var/lib/hyperhive/agents/<name>/`:
- `hyperhive-turn-stats.sqlite` — per-turn timing stats. - `hyperhive-turn-stats.sqlite` — per-turn timing stats.
- `hyperhive-model` — single-line model name override file. - `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 An agent holding the `ManageRootAgent` capability gets every other
**read-write** and its `config` dir **read-only** agent's `state` dir bind-mounted **read-write** and its `config` dir
(`bind_child_agent_dirs` in `lifecycle/host_config.rs`). The RW on **read-only** (`bind_child_agent_dirs` in `lifecycle/host_config.rs`).
`state` is deliberate, not an oversight: a parent manages its children, The RW on `state` is deliberate, not an oversight: the holder recovers
which includes writing into a child's state for recovery (for example seeding other agents, which includes writing into their state (for example
notes, clearing a stuck sentinel) as well as reading it. 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 material — `bash-tasks/`, the turn-stats and event sqlite dbs — and
nothing argues for a parent reading it, let alone writing it. hive-c0re nothing argues for anyone else reading it, let alone writing it.
reads a child's harness dir **directly on the host** when it wants hive-c0re reads a harness dir **directly on the host** when it wants
those stats, which needs no mount into the parent. those stats, which needs no mount into another container.
<!-- vale write-good.Passive = NO --> <!-- vale write-good.Passive = NO -->
**`config` is read-only, including for the parent.** A config change is **`config` is read-only, including for the holder.** A config change is
a PR on the child's config repo, made from a clone and merged after 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 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 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 path to the same file that skips the review entirely, which makes the
@ -361,15 +366,15 @@ boundary a convention rather than a permission.
<!-- vale write-good.Passive = NO --> <!-- vale write-good.Passive = NO -->
⚠️ Don't confuse it with the config-repo seeding hive-c0re does at ⚠️ 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 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 — constrains writers *inside* a container only. The two are unrelated —
conflating them can lead you to reason your way into thinking this conflating them can lead you to reason your way into thinking this
mount should be writable when it shouldn't. mount should be writable when it shouldn't.
<!-- vale write-good.Passive = YES --> <!-- vale write-good.Passive = YES -->
Per-child isolation still holds: a container only ever has its *own* Isolation still holds by default: a container has its *own* dirs and,
dirs plus its direct children's bind-mounted, never a sibling's. unless it holds the capability, nothing else.
Under `/var/lib/hyperhive/applied/<name>/` — the hive-c0re-only Under `/var/lib/hyperhive/applied/<name>/` — the hive-c0re-only
applied repo. Tracks `flake.nix` (module-only boilerplate; never applied repo. Tracks `flake.nix` (module-only boilerplate; never
@ -387,13 +392,11 @@ Contents:
`nixosConfigurations.<n>` output per agent. `flake.lock` is the `nixosConfigurations.<n>` output per agent. `flake.lock` is the
canonical "what's deployed where." The git log is the deploy canonical "what's deployed where." The git log is the deploy
audit trail (one commit per successful deploy or hyperhive bump). audit trail (one commit per successful deploy or hyperhive bump).
- `topology.json` — parent/child agent graph - `topology.json` — the agent roster (`["alice", "bob", "ruth"]`).
(`{ "alice": "root", "bob": "alice", "root": null }`). Written by `topology::reconcile` on every meta sync; read by
Written by `topology::apply_set_parent` (the pure move-validating `topology::all_agents`, which is the set the `ManageRootAgent`
transform) via `meta::bulk_commit_topology` (the committer — see the capability grants mounts over. Carried a `parent` per agent until
`Reparent` node in [`docs/scheduler/coordinator.md`](../scheduler/coordinator.md)); read by #4472; the reader still accepts that shape and keeps its keys.
the dashboard, the renderer, and `<parent>` / `<children>` recipient
resolution.
- `tool-groups.json` — per-agent MCP tool group grants - `tool-groups.json` — per-agent MCP tool group grants
(`{ "alice": ["messaging", "inbox", "execution"] }`). Written by (`{ "alice": ["messaging", "inbox", "execution"] }`). Written by
`tool_groups::set_groups`; injected as `HIVE_TOOL_GROUPS` env `tool_groups::set_groups`; injected as `HIVE_TOOL_GROUPS` env

View file

@ -270,7 +270,7 @@ See [`approvals.md`](../agent-lifecycle/approvals.md) for the full flow.
### 8 · Useful host commands ### 8 · Useful host commands
```bash ```bash
# Roster: all agents, status, rev, parent, pending reminders # Roster: all agents, status, rev, pending reminders
hivectl list-agents hivectl list-agents
# Restart a stuck container (no rebuild) # Restart a stuck container (no rebuild)

View file

@ -87,24 +87,13 @@ angle-bracket and asterisk shapes below are structurally safe.
(`socket_server::handle_send` fans out via `Coordinator::broadcast_send`). (`socket_server::handle_send` fans out via `Coordinator::broadcast_send`).
- `operator` — the human at the dashboard. Messages accumulate in the - `operator` — the human at the dashboard. Messages accumulate in the
inbox view; no agent ever `recv`'s them. inbox view; no agent ever `recv`'s them.
- `<parent>` — 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.
- `<children>` — 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 `<children>` or `<parent>` send resolves to real recipients, the `<parent>` and `<children>` were two more, resolved against a
broker stores the *resolved* labels as the message recipients — the `topology.json` parent field. #4472 removed that field and both
dashboard and recv side see the real routes. The sentinels are purely sentinels with it: address `operator` where you would have said
send-time addressing conveniences. `<parent>`, and name the recipients (or broadcast to `*`) where you
would have said `<children>`. Nothing rewrites a recipient at send time
any more — what an agent passes is what the broker stores.
## Wire protocol ## Wire protocol

View file

@ -55,37 +55,36 @@ Cheap — no build slot:
<!-- vale write-good.Passive = NO --> <!-- vale write-good.Passive = NO -->
| Node | Behavior | | 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 | | `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/<id>` 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 | | `DeployTail` | the deploy's `AfterAny` compensation + bookkeeping tail: (1) rolls `applied/main` back from the parked `refs/hyperhive/rollback/<id>` 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 | | `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#<name>` ref resolves. Store/meta-only, no container yet | | `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#<name>` 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 | | `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 | | `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 | | `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 | | `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 | | `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_ | | `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 | | `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 | | `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 | | `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` | | `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 | | `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 | | `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 | | `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 | | `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/<id>`, 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 | | `FinalizeDeploy` | deploy phase 3 — drop the rollback ref, plant `deployed/<id>`, 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 | | `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 | | `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 | | `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 | | `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<Ident>)` 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 |
| `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 |
| `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 |
| `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 |
| `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 |
| `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 |
<!-- vale write-good.Passive = YES --> <!-- vale write-good.Passive = YES -->
@ -100,7 +99,7 @@ container build:
- **The deploy window** (`Resource::MetaWindow`): a global, capacity-1 queue - **The deploy window** (`Resource::MetaWindow`): a global, capacity-1 queue
resource declared by every node kind that mutates the meta repo — `MetaSync`, 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 `DeployWindow` — the deploy subtree's root, which holds it across every
phase below it (it declares `Resource::MetaWindow`). Two meta phase below it (it declares `Resource::MetaWindow`). Two meta
mutations can therefore never interleave, so no commit lands inside another 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 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; 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) plus Reconcile(a) for every drifted agent (all ONE DAG)
reparent(moves): Reparent(moves) (no rebuild — topology.json is read live)
``` ```
Notable collapses: Notable collapses:
@ -229,7 +227,7 @@ resources are free. Resources:
(`SetWanted` is a store write, not a container op, but takes the lease anyway (`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 so a power-op DAG's intent write + reconcile is atomic — two racing ops can't
clobber intent before either reconciles.) **Lease-exempt**: `MetaSync`, 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 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 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`, 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` 1. **Config path** — when _any_ marker is stale, submit one `Boot`
DAG: a `MetaLock` (hyperhive input bump, non-fatal) that grows an in-DAG DAG: a `MetaLock` (hyperhive input bump, non-fatal) that grows an in-DAG
`Rebuild` subgraph for each stale agent whose `wanted = Up` (topology-sorted, `Rebuild` subgraph for each stale agent whose `wanted = Up` (name-sorted). Stale but wanted-offline agents get no boot-time nix work — their
parents first). Stale but wanted-offline agents get no boot-time nix work — their
rebuild happens on their next start (the start submit path upgrades a 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 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 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/approvals.md` — approval flow + scheduled prompts
- `docs/agent-lifecycle/persistence.md` — SQLite schema, state-dir layout - `docs/agent-lifecycle/persistence.md` — SQLite schema, state-dir layout
- `docs/process/conventions.md` — wire protocol, recipient sentinels - `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

View file

@ -31,7 +31,6 @@ This document contains the help content for the `hivectl` command-line program.
* [`hivectl agent kill`↴](#hivectl-agent-kill) * [`hivectl agent kill`↴](#hivectl-agent-kill)
* [`hivectl agent destroy`↴](#hivectl-agent-destroy) * [`hivectl agent destroy`↴](#hivectl-agent-destroy)
* [`hivectl agent rebuild`↴](#hivectl-agent-rebuild) * [`hivectl agent rebuild`↴](#hivectl-agent-rebuild)
* [`hivectl agent set-parent`↴](#hivectl-agent-set-parent)
* [`hivectl agent set-limits`↴](#hivectl-agent-set-limits) * [`hivectl agent set-limits`↴](#hivectl-agent-set-limits)
* [`hivectl agent choom`↴](#hivectl-agent-choom) * [`hivectl agent choom`↴](#hivectl-agent-choom)
* [`hivectl agent watch`↴](#hivectl-agent-watch) * [`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 * `kill` — Hard-stop this managed container
* `destroy` — Tear down this sub-agent container, keeping its state by default. No undo * `destroy` — Tear down this sub-agent container, keeping its state by default. No undo
* `rebuild` — Apply pending config to this managed container * `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 * `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 * `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 * `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 <PARENT>` — New parent agent name. Mutually exclusive with `--root`
* `--root` — Promote this agent to root (no parent)
## `hivectl agent set-limits` ## `hivectl agent set-limits`
Declare this agent's CPU/memory limits, overriding the hive-wide defaults. Declare this agent's CPU/memory limits, overriding the hive-wide defaults.

View file

@ -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 — `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` (`running` / `stopped`, plus ` paused` / ` needs-login` / ` needs-update`
when set — `paused` is orthogonal to running, see below); 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 REV is the first 12 chars of the agent's locked config sha; REMIND is the
place in the topology tree (`-` for a root agent); REMIND is the count of count of pending reminders. It reuses the same per-agent aggregation the dashboard
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 renders, so the CLI roster and the web UI never drift. `--json` emits the
raw rows instead of the table. raw rows instead of the table.

View file

@ -111,9 +111,7 @@ merge work.
Plugin install failures aren't fatal: each entry comes back as a Plugin install failures aren't fatal: each entry comes back as a
human-readable failure string that gets routed via human-readable failure string that gets routed via
`Surface::send_to_parent` to the agent's topology parent (the `Surface::send_to_operator` to the operator.
broker resolves `<parent>` per `topology::resolve_recipient`; root
agents and the manager fall through to operator).
### Turn outcomes ### 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(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(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(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] \`<qualified-label>\` claude turn failed:\n<err>` to `<parent>` via `send_to_parent` | | `Err(Failed(err))` | route `[system] \`<qualified-label>\` claude turn failed:\n<err>` to `operator` via `send_to_operator` |
`ApiStall` catches an Anthropic API stall — a multi-retry connection storm where `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 the stream goes silent for minutes. The idle watchdog lives in `hive-claude`'s

View file

@ -29,15 +29,13 @@ are opt-in via the P3RM1SS10NS tab.
`recv(max?)`, `ack_until(up_to)`. `recv(max?)`, `ack_until(up_to)`.
- `send` — message a peer (logical name) or the operator - `send` — message a peer (logical name) or the operator
(`to: "operator"`). Use `to: "<parent>"` to address the topology (`to: "operator"`). Optional `in_reply_to: i64` links the
parent without hardcoding the label; the broker resolves the
sentinel at delivery time. Optional `in_reply_to: i64` links the
message to a prior id for thread rendering. Per-agent message to a prior id for thread rendering. Per-agent
`services.hyperhive.agent.allowedRecipients` (default: empty = unrestricted) limits `services.hyperhive.agent.allowedRecipients` (default: empty = unrestricted) limits
which names `send` accepts — useful for sandboxing: set which names `send` accepts — useful for sandboxing: set
`[ "operator" ]` to restrict a sub-agent to operator messages only `[ "operator" ]` to restrict a sub-agent to operator messages only.
(the topology parent is always reachable regardless of this list — The operator stays reachable regardless of this list, so an agent can
that carve-out is structural, keyed on parent relationship, not name). always report a block.
- `recv` — drain inbox. Always an immediate peek, never blocks. `max` - `recv` — drain inbox. Always an immediate peek, never blocks. `max`
(default 1, cap 5) drains up to N rows. `recv` prefixes each returned row (default 1, cap 5) drains up to N rows. `recv` prefixes each returned row
with `[msg #<id>]` (broker row id; note the highest id seen, then pass with `[msg #<id>]` (broker row id; note the highest id seen, then pass

View file

@ -56,16 +56,12 @@ surfaces, not tab panes.
## SW4RM tab ## SW4RM tab
**C0NTAINERS** — live containers rendered as a depth-first **C0NTAINERS** — live containers rendered as a flat,
tree using `ContainerView.parent` (populated by alphabetically-sorted list. The renderer still walks
`hive-c0re/src/agent_config/topology.rs` — don't confuse it with `ContainerView.parent` for a depth-first tree with ASCII glyphs
`hive-c0re/src/dashboard/topology.rs`, which only holds the (`├─`, `└─`, `│ ` continuation columns), but #4472 removed that field,
set-parent endpoints). so every row sits at depth 0 and the renderer paints no glyph. The tree
ASCII tree glyphs (`├─`, `└─`, `│ ` continuation columns) prefix sorts alphabetically;
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;
roots likewise. The tree tolerates cycles in the parent graph — roots likewise. The tree tolerates cycles in the parent graph —
it appends orphaned containers (not reachable from any root) it appends orphaned containers (not reachable from any root)
as roots so no agent disappears. Pulsing red banner at the top 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. summaries are sticky (select to dismiss) so they aren't missed.
Every destructive run-state action (`ST0P`, `R3ST4RT`, `R3BU1LD`, 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. the per-agent `⋮` menu and the bulk selection bar.
**Graceful stop** — the `■ ST0P` confirm dialog (per-agent and **Graceful stop** — the `■ ST0P` confirm dialog (per-agent and
@ -985,10 +981,12 @@ identically.)
### Topology tree ### Topology tree
See **SW4RM tab** above for the parent/child derivation, sibling ⚠️ **Dormant since #4472,** which removed `ContainerView.parent`:
sort order, and cycle-safety rules (`swarm.js::buildAgentTree` walks `buildAgentTree` now puts every container at depth 0 and the rules below are
`ContainerView.parent`) — this section covers only how the tree is all no-ops — the list renders flat. The walk and the prefix painting are
*drawn*. 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 The per-row prefix column (`.tree-prefix`) is **DOM-painted, not
text-glyph-painted**: each indent lane is its own positioned `<span>` text-glyph-painted**: each indent lane is its own positioned `<span>`
@ -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 the row's icon midline). Exact lane widths and positioning live in
`swarm.js`'s tree-prefix rendering and its paired CSS rules — not `swarm.js`'s tree-prefix rendering and its paired CSS rules — not
reproduced here since they're tuned in pixel units and will drift. 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 With every container at depth 0 these rules are all no-ops and the
all no-ops and the layout reads like a plain flat list. layout reads like a plain flat list, which is what a hive renders today.
## Selection bar ## Selection bar
@ -1032,25 +1030,10 @@ frosted-mauve bar slides up from the bottom of the viewport
- `▶ R3SUM3` — paused agents only - `▶ R3SUM3` — paused agents only
- `↻ R3BU1LD` — always available - `↻ R3BU1LD` — always available
- `DESTR0Y` / `PURG3` — 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=<name>&new_parent=<target>`)
- **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)
<!-- vale write-good.Passive = YES --> <!-- vale write-good.Passive = YES -->
Both write `topology.json` and re-emit a container snapshot so #4472 removed the `M0V3` picker that used to sit here — an agent has
the tree repaints without a page reload. no parent to move it to any more.
- **`✕ clear`** button + `Esc` key clear the entire selection. - **`✕ clear`** button + `Esc` key clear the entire selection.
Every render prunes stale selections (agents destroyed while Every render prunes stale selections (agents destroyed while

View file

@ -952,7 +952,6 @@ export function renderSelectionBar(containers) {
confirm: (names) => confirm: (names) =>
`PURGE ${names.length} agent${names.length === 1 ? "" : "s"} (${names.join(", ")})? containers, config history, claude creds, and notes are all WIPED. no undo.`, `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) { function addBulkButton(parent, btnClass, label, enabled, selected, opts) {

View file

@ -192,7 +192,10 @@ mod tests {
#[test] #[test]
fn all_agents_in_returns_every_name_sorted() { 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] #[test]

View file

@ -151,9 +151,9 @@ in
Place the file inside the agent's bind-mounted **harness** dir (e.g. Place the file inside the agent's bind-mounted **harness** dir (e.g.
`/agents/<name>/harness/openrouter.env`, `$HYPERHIVE_HARNESS_DIR`), `/agents/<name>/harness/openrouter.env`, `$HYPERHIVE_HARNESS_DIR`),
not `state/` `harness/` survives container rebuilds exactly like not `state/` `harness/` survives container rebuilds exactly like
`state/` does, but is never bind-mounted into a parent agent's `state/` does, but is never bind-mounted into another agent's
container (unlike `state/`, which a parent gets read-write for child container (unlike `state/`, which a `ManageRootAgent` holder gets
recovery see `docs/agent-lifecycle/persistence.md`'s "Parent access to child 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 state"), so this credential is reachable by nothing but this agent
and the host. Permissions should be `0600`, owned by the agent's and the host. Permissions should be `0600`, owned by the agent's
unix user. Loaded with a leading `-` (optional `EnvironmentFile`), unix user. Loaded with a leading `-` (optional `EnvironmentFile`),

View file

@ -183,20 +183,21 @@ in
mkdir -p "$(dirname "$marker")" mkdir -p "$(dirname "$marker")"
: > "$marker" : > "$marker"
# Scope state + harness chowns to THIS container's own dirs only. # Scope state + harness chowns to THIS container's own dirs only.
# The glob `/agents/*/state` also matches child-agent state dirs that # The glob `/agents/*/state` also matches other agents' state dirs
# are bind-mounted into parent containers, which would clobber the # bind-mounted into a `ManageRootAgent` holder's container, which
# ownership those dirs' own activation scripts set — producing # would clobber the ownership those dirs' own activation scripts set
# intermittent EACCES for the child agent's harness between a parent # — producing intermittent EACCES for that agent's harness between
# rebuild and the child's next activation. Config dirs are kept broad # the holder's rebuild and its own next activation. Config dirs are
# because the parent legitimately owns child proposed-config repos. # kept broad because the holder legitimately owns the proposed-config
# repos it edits.
if [ -d "/agents/$userName/state" ]; then if [ -d "/agents/$userName/state" ]; then
chown -hR "$userName:$userName" "/agents/$userName/state" 2>/dev/null || true chown -hR "$userName:$userName" "/agents/$userName/state" 2>/dev/null || true
fi fi
if [ -d "/agents/$userName/harness" ]; then if [ -d "/agents/$userName/harness" ]; then
chown -hR "$userName:$userName" "/agents/$userName/harness" 2>/dev/null || true chown -hR "$userName:$userName" "/agents/$userName/harness" 2>/dev/null || true
fi fi
# The proposed-config repo is RW-mounted into the editing (parent/ # The proposed-config repo is RW-mounted into the editing agent and
# manager) agent and owned by it; hive-c0re only pulls from it. Heal # owned by it; hive-c0re only pulls from it. Heal
# it to this user too — same as state/harness. In an agent's own # 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 # container its config is RO-mounted, so the chown there just fails
# harmlessly (|| true). # harmlessly (|| true).

View file

@ -60,11 +60,7 @@
# against hive names, in `swarm-otel.nix`'s own `reservedOwners`. # against hive names, in `swarm-otel.nix`'s own `reservedOwners`.
"swarm" "swarm"
] ]
# Deliberately absent, and both are load-bearing omissions: # Deliberately absent, and it is a load-bearing omission:
#
# `<parent>` / `<children>` — routing recipients the ident charset already
# rejects, so no name can ever equal them; listing them would imply a guard
# that never fires.
# #
# `ruth` — a real agent, not a literal. A second agent wanting that name is a # `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. # name that is TAKEN, which the roster answers, not this list.