Apply contraction fixes across ~40 doc files (setup, integrations, lifecycle, networking, scheduler, swarm, tools, trust-boundary, UI, etc.). Skipped 14 hits: - 10 where words appear in ALL CAPS for deliberate emphasis (is NOT, do NOT, etc.) - 4 where text could not be safely located due to markdown formatting or column position Applied via systematic scan with checks for fenced code blocks, inline code spans, and intentional caps. Preserves sentence-initial capitalization throughout.
244 lines
12 KiB
Markdown
244 lines
12 KiB
Markdown
# Agent hierarchy & privileges
|
|
|
|
Every agent has a place in an operator-editable parent/child tree, used
|
|
to scope which agents can manage which others. This doc covers how the
|
|
tree is stored and edited 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. Tracking issue: hyperhive#361
|
|
(`$HIVE_FORGE_URL/hyperhive/hyperhive/issues/361`).
|
|
|
|
## Where the tree lives
|
|
|
|
Topology 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"
|
|
}
|
|
```
|
|
|
|
`null` = root-level agent. New agents **default to root** — there is no
|
|
structural manager that everything hangs under. Hierarchy is built
|
|
explicitly: an agent that requests a sub-agent gets a
|
|
requester-as-parent edge written at its `init_config` approval (so
|
|
`bob` above was spawned by `alice`), and the operator can reparent any
|
|
agent, including the bootstrap container (`ruth`) — 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
|
|
|
|
- CLI: `hivectl agent <child> set-parent --parent <new>` (or `--root`
|
|
to promote). Exactly one of `--parent` / `--root` is required.
|
|
- 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:
|
|
|
|
- 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.
|
|
|
|
### 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.
|
|
|
|
### 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).
|
|
- **Reconcile** — runs alongside the periodic meta/flake regeneration.
|
|
New agents default to root unless they already carry an explicit
|
|
parent edge from an `init_config` approval; existing entries
|
|
(including operator overrides) are preserved; removed agents drop.
|
|
Agents that are approved but not yet spawned keep their edge too, so
|
|
it survives the gap until the container actually appears.
|
|
- **Inject** — each container's parent (if any) is exposed 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
|
|
module docs for the exact call chain.
|
|
|
|
### Current limitation: state-dir visibility lags topology
|
|
|
|
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.
|
|
|
|
## Planned topology semantics (once ancestor-based enforcement lands)
|
|
|
|
| operation | who can do it |
|
|
| ----------------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
|
| `kill` / `start` / `restart` / `update` (any descendant) | any ancestor |
|
|
| `request_init_config` (spawn a new child) | any agent, child added under self |
|
|
| config change via forge PR (any descendant's config) | any ancestor |
|
|
| `get_logs` (any descendant) | 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 |
|
|
| `request_update_meta_inputs` (bump meta lock) | root agents only (today: just `manager`) |
|
|
|
|
"Ancestor" walks `ContainerView.parent` chains; cycles are guarded by a
|
|
visited-set at dispatch time (a malformed `topology.json` can't lock
|
|
the dispatcher into a loop).
|
|
|
|
## Manager special-casing today
|
|
|
|
Enforcement of the ancestor rules above isn't fully wired yet, so the
|
|
**manager (`ruth`) still gets some hard-coded special treatment**
|
|
other agents don't:
|
|
|
|
- **Naming/bootstrap** — the manager's broker recipient name, state-dir
|
|
key, and nixos-container name are all `ruth` (container `h-ruth`).
|
|
`hive-c0re` spawns it directly at boot if missing, with no operator
|
|
approval step — every other agent goes through `request_init_config`
|
|
→ approval. Topology-wise, `ruth` is still just another root agent.
|
|
- **Wire-protocol** — the privileged `Request` variants
|
|
(`RequestInitConfig`; `Kill` / `Start` / `Restart` / `Update`;
|
|
`GetLogs`; `RequestUpdateMetaInputs`) — 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 agent, child added under self" for init-config,
|
|
"any ancestor" for lifecycle/logs); `RequestUpdateMetaInputs` stays
|
|
a root-only capability even post-milestone, not a topology rule.
|
|
One exception: `Wake` (inject a `from: <X>` message into the
|
|
caller's own inbox) isn't really privileged — every per-agent daemon
|
|
(e.g. `hive-forge-notify`) needs it, and sub-agents already have the
|
|
equivalent on their own socket.
|
|
- **Storage/mounts** — only the manager container gets
|
|
`/var/lib/hyperhive/agents` bind-mounted RW at `/agents` (so it can
|
|
manage any agent's state dir — config isn't authored there, since a
|
|
real config change is a PR from a clone), plus RO mounts for
|
|
`/applied` (diff against what's deployed) and `/meta` (system-wide
|
|
deploy log). 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. RO `/meta`
|
|
access will be gated on a "meta read" capability; only
|
|
`request_update_meta_inputs` writes `flake.lock`, gated by its own
|
|
capability.
|
|
- **Prompt/tools** — the system prompt uses `<!-- role:agent -->` /
|
|
`<!-- role:manager -->` marker blocks, and a `Flavor::{Agent,
|
|
Manager}` switch picks the MCP tool allow-list claude sees. Both are
|
|
already parametrised on a single flavour value, so the planned
|
|
per-capability-group version (`cap:<group>` prompt blocks + a
|
|
matching tool allow-list) is additive rather than a rewrite.
|
|
- **State dirs** — _not_ special-cased: `HYPERHIVE_STATE_DIR` is
|
|
injected uniformly via `systemd.globalEnvironment` for every
|
|
container including the manager, so all token/state paths resolve
|
|
through it the same way everywhere.
|
|
- **Scattered ownership checks** — a handful of independent
|
|
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 auto-restarts 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
|
|
module docs for `loose_ends.rs`, `stores/broker.rs`, `actions.rs`,
|
|
and `workers/crash_watch.rs` for the current owner-check logic in
|
|
each. (Reminder cancellation is handled fully in-agent — see the note
|
|
on `CancelLooseEndKind::Reminder` in
|
|
`hive-c0re/src/socket_server/mod.rs`.)
|
|
|
|
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`
|
|
sibling), one shared `nix/agent-modules/` tree, one service unit
|
|
(`systemd.services.hive-agent`) for all agents. There is no separate
|
|
manager service name or role distinction in the harness — privilege
|
|
differences live server-side in the broker socket (which tool groups
|
|
and manager-surface calls each agent receives).
|
|
|
|
`agent.nix` and `ruth.nix` both import the shared `nix/agent-modules/`.
|
|
`ruth.nix` additionally sets forge defaults to suppress the
|
|
subscription/participation firehose so ruth's inbox stays focused on
|
|
direct mentions, reviews, and assignments.
|
|
|
|
### Environment variables set on the unit
|
|
|
|
- `HOME = /home/<userName>` — systemd defaults `HOME` to `/` for
|
|
services without `User=` set; with the per-agent user the harness
|
|
needs the right home so claude finds its bind-mounted `~/.claude/`
|
|
session dir.
|
|
- `HIVE_STATIC_DIR = <mergedDist>` — `tower_http::ServeDir` root for
|
|
the per-agent web UI; merged dist = agent default + every
|
|
`hyperhive.frontend.extraFiles` overlay.
|
|
- `HIVE_ASSETS_DIR = pkgs.hyperhive-assets/share/hyperhive` — set
|
|
directly on the unit, **not** via `environment.variables`, because
|
|
the latter only populates `/etc/profile` which systemd services
|
|
don't inherit.
|
|
|
|
### `PATH` setup (the wrapper-dir trick)
|
|
|
|
```nix
|
|
path = [ "/run/wrappers" "/run/current-system/sw" ];
|
|
```
|
|
|
|
`/run/wrappers` (not `/run/wrappers/bin`) comes first so setuid
|
|
wrappers — notably `sudo` — resolve before bare nix-store binaries; see
|
|
[`docs/process/gotchas.md`](../process/gotchas.md) ("`systemd.services.*.path` appends
|
|
`/bin` to every entry") for why the trailing `/bin` matters in
|
|
general. It's load-bearing here because the harness runs as the
|
|
per-agent user: without the wrapper dir on `PATH`, `sudo` resolves to
|
|
the non-setuid nix-store binary and every
|
|
`hyperhive.user.passwordlessSudo` grant fails with "must be owned by
|
|
uid 0 and have the setuid bit set."
|
|
|
|
### `serviceConfig` highlights
|
|
|
|
- `ExecStart = pkgs.hyperhive/bin/hive-agent` — same binary for every
|
|
agent.
|
|
- `Restart = on-failure`, `RestartSec = 2` — keeps the harness
|
|
resilient across transient crashes without thundering retries.
|
|
- `RuntimeDirectory = "hive-config"` → `/run/hive-config/` owned by
|
|
`User=`, auto-cleared on stop. The harness writes regenerated
|
|
`claude-{mcp-config,settings,system-prompt}` files there
|
|
(`paths::config_dir`). Deliberately separate from `/run/hive`, which
|
|
the host bind-mounts in root-owned and which holds hive-c0re's
|
|
`mcp.sock`.
|
|
- `User = Group = userName` — drops root inside the container; sudo is
|
|
the explicit escalation surface (`hyperhive.user.passwordlessSudo`).
|
|
|
|
## Cross-references
|
|
|
|
- Milestone: "Agent privileges and sub-agents"
|
|
(`$HIVE_FORGE_URL/hyperhive/hyperhive/issues/361`)
|
|
- Dashboard render: "show agent topology in container list"
|
|
(`$HIVE_FORGE_URL/hyperhive/hyperhive/issues/363`)
|
|
- Audit table source: milestone comment
|
|
(`$HIVE_FORGE_URL/hyperhive/hyperhive/issues/361#issuecomment-3335`)
|
|
- Operator/agent trust boundary (orthogonal axis): [`boundary.md`](../trust-boundary/boundary.md)
|