hyperhive/docs/agent-lifecycle/agent-hierarchy.md
atlas b88a5b2430 remove the list_containers and request_update_meta_inputs MCP tools
Both agent-facing tools go away end to end, with no replacement. This is
an intentional capability removal: agents can no longer enumerate their
own subtree, and can no longer queue a meta-flake input bump.

The system prompt and docs/tools/lifecycle.md land in this same commit
on purpose. A tool named in the prompt but absent from the server makes
agents confidently call something that doesn't exist, and the failure
then surfaces far from its cause.

Removed:

- MCP registrations and bodies (hive-agent-mcp), plus the now-unused
  UpdateMetaInputsArgs.
- Wire variants Request::ListDescendants,
  Request::RequestUpdateMetaInputs and Response::Containers, plus
  ContainerInfo, whose only consumer was that response.
- hive-c0re's handle_list_descendants (its whole module) and
  handle_request_update_meta_inputs, the two dispatch arms, and the
  require_group(agent, "approvals", ...) gate on the meta-inputs verb.
- The stream_enrich emoji entry and argument formatter.
- docs/tools/lifecycle.md (both tools it documented are gone), its two
  referrers, the tool-group tables and the agent-hierarchy prose.

Tool groups are kept, deliberately. ToolGroup::Lifecycle listed exactly
one tool and now lists none — it is vestigial, but the variant stays so
existing meta/capabilities.json grants still parse; retiring it is a
separate decision. ToolGroup::Approvals also listed exactly one tool,
but the group is NOT dead: check_can_cancel_approval still gates
cancel_loose_end's approval-cancel arm on it server-side.

ApprovalKind::UpdateMetaInputs stays too. Nothing in production code
produces it any more, but pre-existing approval rows may still carry it,
and the operator's own path to a meta update is unaffected — the
dashboard's POST /api/meta-update inserts the meta_update job directly,
bypassing approvals entirely.

The two format_ack tests in hive-agent-mcp that named
request_update_meta_inputs were only using it as a label string while
exercising the generic OkWarn/Ok renderer, so they are retargeted to a
surviving tool rather than deleted.

Note hive-c0re's priv_client::list_containers is a different thing (the
host-side privileged container listing behind hive-priv) and is
untouched.

Closes #4591
2026-09-20 22:47:46 +02:00

12 KiB

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 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.

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

Reparenting

  • 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> }.

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 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.

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

"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).

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 a Spawn approval. Topology-wise, ruth is still just another root agent.
  • 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 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.
  • 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. 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 /api/meta-update as the only entry point.
  • 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 dirsnot 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 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 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 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. 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

  • 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 services.hyperhive.agent.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)

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 services.hyperhive.agent.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=, autocleared 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 (services.hyperhive.agent.user.passwordlessSudo).

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