hyperhive/docs/agent-hierarchy.md

9.7 KiB

Agent hierarchy & privileges

Design + audit doc for milestone #6 (the issue tree). The implementation lands in pieces; this doc tracks what's done, what's planned, and what currently special-cases the manager.

Current state (as of this PR)

Topology lives in the hive-c0re-owned meta repo, alongside flake.nix, at /var/lib/hyperhive/meta/topology.json:

{
  "manager": null,
  "alice":   "manager",
  "bob":     "alice"
}

null = root-level agent. Today only the manager qualifies by default. Other agents land under "manager" on first sync. Re-parenting is operator-driven (#486 / #487):

  • CLI: hive-c0re set-parent <child> --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 converge on topology::set_parent, which delegates the validation rules to a pure apply_set_parent helper. Refuses:

  • reparenting the manager (structurally root),
  • unknown child / new_parent (typo guard),
  • self-parenting,
  • cycles (32-hop ancestor walk, mirroring is_descendant_of).

Idempotent no-op fast path skips the disk write when the parent is already what's requested. After a successful write the surfaces call Coordinator::rescan_containers_and_emit so connected dashboard viewers see the tree repaint without polling (ContainerView.parent is sourced from topology.json).

Today's caveat (#361 follow-up): the move is purely a JSON edit. Only the top-level manager (hm1nd) 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. Once sub-manager bind mounts land alongside #361 enforcement, set_parent grows a companion umount-old / mount-new / restart-cascade step (tracked via the cross-ref comment on #361).

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.

Flow

  1. Read: topology::read() parses topology.json into a BTreeMap<String, Option<String>>. Missing / unparsable file → empty map → every agent treated as root (safe degradation for fresh installs that haven't run meta::sync_agents yet).
  2. Reconcile: meta::sync_agents calls topology::reconcile alongside its flake.nix regeneration. New agents land at their default position (manager as parent, manager itself as root); removed agents drop. Existing entries are preserved as-is so operator overrides stick across regenerations.
  3. Inject: meta::render_flake looks up each agent's parent and passes it to mkAgent. When non-null, the mkAgent body sets HIVE_PARENT = parent in the agent's systemd service environment so the harness / claude prompts can see it.
  4. Surface: container_view::build_all reads topology.json and populates ContainerView.parent: Option<String> on every rescan. The dashboard renders the field as a tree (#363 follow-up).

Target topology semantics

Once enforcement lands the rules collapse into:

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
request_apply_commit (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).

Current manager special-casings — the audit

What currently makes the manager different from every other agent, and which axis the post-milestone version reads each special-case along:

A — naming + bootstrap

  • MANAGER_AGENT = "manager" (broker recipient name) and MANAGER_NAME = "hm1nd" (container name). ~28 grep hits across hive-c0re/src/. Just a name — the rename plan is managerroot, executed via the one-shot migration script in migrate.rs (idempotent, marker-guarded).
  • auto_update::ensure_manager runs at hive-c0re boot and spawns hm1nd if missing. Becomes "ensure the root agent exists" once any agent can be at the root. Topology: root has no parent, so hive-c0re itself owns its lifecycle (no parent to delegate to).

B — wire-protocol privileges

The ManagerRequest::* variants in hive-sh4re/src/lib.rs are operations the manager flavour socket can make that sub-agent sockets can't:

variant semantic post-milestone
RequestInitConfig seed an agent's proposed config repo topology — descendants only
RequestApplyCommit submit a commit sha for operator approval topology — descendants only
Kill / Start / Restart / Update container lifecycle on an existing agent topology — descendants only
RequestUpdateMetaInputs bump meta flake.lock per-agent cap (root-only today; a future "let coder bump its own input" might grant it)
GetLogs journalctl scrape of a sub-agent topology — descendants only
Wake inject a from: <X> message into self's inbox not really privileged — the wire surface exists because daemon co-processes (e.g. forge_notify) need it. Sub-agents have the same via their own socket.

C — storage / mounts (hive-c0re::lifecycle)

The manager container's nspawn bind set:

  • HOST_AGENTS_ROOT (/var/lib/hyperhive/agents) → /agents RW — so the manager can edit any agent's proposed config repo
  • HOST_APPLIED_ROOT (/var/lib/hyperhive/applied) → /applied RO — so the manager can diff against what's deployed
  • HOST_META_ROOT (/var/lib/hyperhive/meta) → /meta RO — so the manager can read the system-wide deploy log

Tree-shape version:

  • Each agent gets RW to /agents/<descendant>/ for every descendant in its subtree. The root agent (today: manager) gets RW to the full forest as a special case of "the root has every other agent as a descendant".
  • RO /meta access if the agent holds a "meta read" cap.
  • request_update_meta_inputs is the only path that actually writes flake.lock, gated by the cap; everyone else stays RO.

D — drop legacy /state for manager ✓ done (#604)

lifecycle.rs no longer binds /state for the manager. HYPERHIVE_STATE_DIR is now injected uniformly via systemd.globalEnvironment in meta.rs for every container (manager included), so all token/state paths resolve through $HYPERHIVE_STATE_DIR. The harness-base shell scripts (tea-login, forge-avatar-sync, matrix-avatar-sync) simplified from glob+for loops to a direct $HYPERHIVE_STATE_DIR/<token> read.

E — prompt + tools

  • prompts/system.md with <!-- role:agent --> / <!-- role:manager --> marker blocks, assembled by hive_ag3nt::prompt::render based on flavor (closes #519). Per-agent cap list of what the agent can do — already a single parametrised prompt; once #513 lands the marker grammar grows cap:<group> blocks the renderer reads from the per-agent ToolGroup set.
  • mcp.rs::Flavor::{Agent, Manager} controls which MCP tools claude sees. Already structured this way internally — the per-flavour allow-list becomes a per-cap-set lookup.

F — drive-by checks across c0re

(grep -n MANAGER_AGENT produced ~28 hits)

  • loose_ends.rs: manager sees hive-wide loose-ends, sub-agents only their own. Topology — every agent sees its own + its descendants'.
  • operator_questions.rs + broker.rs: "manager can cancel any question" override on the owner check. Topology — agents can moderate threads of their descendants. (per mara's https://localhost:3000/hyperhive/hyperhive/issues/361#issuecomment-3344)
  • reminder_scheduler.rs: same override pattern for reminder cancel. Topology — descendants only.
  • actions.rs: destroy refuses to act on MANAGER_NAME (no foot-shooting). Topology — agents can destroy descendants but never themselves or ancestors.
  • crash_watch.rs: skips ContainerCrash for the manager (it auto-restarts via systemd). Topology — the root container has different recovery semantics, every other agent falls into the same watch loop.

G — sub-agents inside the same container

Future work mentioned in #361: when enabled for an agent, it can 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?

Cross-references