245 lines
13 KiB
Markdown
245 lines
13 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 questions / 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 is not 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),
|
|
"manager can cancel any question/reminder" overrides on the owner
|
|
check, `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/operator_questions.rs`,
|
|
`stores/broker.rs`, `actions.rs`, and `workers/crash_watch.rs` for
|
|
the current owner-check logic in each. (Reminder cancellation has
|
|
since moved fully in-agent — see the note on
|
|
`CancelLooseEndKind::Reminder` in `hive-c0re/src/questions.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/gotchas.md`](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`](boundary.md)
|