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:

{
  "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

All three go through the same validation, which refuses:

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

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:

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:

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. No separate manager service name or role distinction exists 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

PATH setup (the wrapper-dir trick)

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 ("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

Cross-references