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:
parent
d94bc2188d
commit
179f873722
17 changed files with 186 additions and 262 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,109 +1,97 @@
|
|||
# Agent hierarchy & privileges
|
||||
# Agent roster & privileges
|
||||
|
||||
<!-- vale write-good.Passive = NO -->
|
||||
|
||||
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.
|
||||
|
||||
<!-- 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`:
|
||||
|
||||
```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 <child> set-parent --parent <new>` (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<String> }`.
|
||||
### 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 |
|
||||
| -------------------------------------------- | --------------------------------------------------- |
|
||||
| `<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 |
|
||||
| ----------------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
||||
| 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 |
|
||||
<!-- vale write-good.Passive = NO -->
|
||||
|
||||
"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.
|
||||
|
||||
<!-- vale write-good.Passive = YES -->
|
||||
|
||||
## 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: <X>` 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: <X>` 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/<descendant>/`
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -335,24 +335,29 @@ Under `/var/lib/hyperhive/agents/<name>/`:
|
|||
- `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.
|
||||
|
||||
<!-- vale write-good.Passive = NO -->
|
||||
**`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.
|
|||
|
||||
<!-- vale write-good.Passive = NO -->
|
||||
⚠️ 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.
|
||||
<!-- vale write-good.Passive = YES -->
|
||||
|
||||
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/<name>/` — the hive-c0re-only
|
||||
applied repo. Tracks `flake.nix` (module-only boilerplate; never
|
||||
|
|
@ -387,13 +392,11 @@ Contents:
|
|||
`nixosConfigurations.<n>` 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 `<parent>` / `<children>` 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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
- `<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
|
||||
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.
|
||||
`<parent>` and `<children>` 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
|
||||
`<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
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ Cheap — no build slot:
|
|||
<!-- vale write-good.Passive = NO -->
|
||||
|
||||
| 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/<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 |
|
||||
|
|
@ -80,7 +80,6 @@ Cheap — no build slot:
|
|||
| `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<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 |
|
||||
| `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 |
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 <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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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 `<parent>` 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] \`<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
|
||||
the stream goes silent for minutes. The idle watchdog lives in `hive-claude`'s
|
||||
|
|
|
|||
|
|
@ -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: "<parent>"` 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 #<id>]` (broker row id; note the highest id seen, then pass
|
||||
|
|
|
|||
|
|
@ -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 `<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
|
||||
`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=<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 -->
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -151,9 +151,9 @@ in
|
|||
Place the file inside the agent's bind-mounted **harness** dir (e.g.
|
||||
`/agents/<name>/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`),
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -60,11 +60,7 @@
|
|||
# against hive names, in `swarm-otel.nix`'s own `reservedOwners`.
|
||||
"swarm"
|
||||
]
|
||||
# Deliberately absent, and both are load-bearing omissions:
|
||||
#
|
||||
# `<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.
|
||||
# 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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue