docs(approvals): reframe stale 'the manager' as the root agent

The last docs/ piece of the manager-cleanup. The manager is no longer a
structural role — root-ness is purely topological. Reframe:
- title 'Approvals + manager + helper events' -> 'Approvals + helper events'
- section headers: 'Manager view of applied'/'Manager policy'/'Manager
  (ruth) is hive-c0re-managed'/'Helper events to the manager' ->
  root-agent / root-bootstrap-container equivalents
- body prose: 'the manager (ruth)' -> 'the root agent' (or 'the submitter'
  in the approval-flow steps)
- authority semantics: 'manager-only' -> approvals are submitted by an
  agent with the approvals tool group, for its direct children
- dropped the stale 'the manager refuses to destroy itself' line (the
  bootstrap container is now destroyable + transient; recreated on startup)

Kept the genuine code/protocol identifiers (nixosConfigurations.manager,
manager_server, role:manager prompt block, notify_manager, the
/run/hyperhive/manager/ socket path) — renaming those would diverge from
the source (de-hardcoding is its own backend cleanup).
This commit is contained in:
iris 2026-06-23 17:56:32 +02:00 committed by mara
commit e18ddff0b0

View file

@ -1,17 +1,20 @@
# Approvals + manager + helper events
# Approvals + helper events
The approval queue is hyperhive's pivot: nothing that changes the
shape of an agent (its config, whether it exists) happens without an
operator click. The manager (`ruth`) is the policy gate in front of
that queue; helper events are how it stays informed about what
happens after a decision lands.
operator click. The submitting agent — any agent with the `approvals`
tool group, which manages the config of its **direct children** (the
root agent for top-level agents; a sub-manager for its own subtree) — is
the policy gate in front of that queue; helper events are how it stays
informed about what happens after a decision lands.
## End-to-end approval flow
1. Manager edits files under `/agents/<name>/config/` (any tracked
path, but `agent.nix` is the contract entry point) and commits
with its own git identity.
2. Manager submits the commit sha via `request_apply_commit(agent,
1. The submitting agent (the child's parent, holding the `approvals`
tool group) edits files under the child's `/agents/<name>/config/`
(any tracked path, but `agent.nix` is the contract entry point) and
commits with its own git identity.
2. The submitting agent submits the commit sha via `request_apply_commit(agent,
commit_ref)`. `commit_ref` must be a commit **sha** (7-40 hex
chars, short or full) — a branch or tag name is rejected so the
approval pins an immutable commit.
@ -22,9 +25,9 @@ happens after a decision lands.
resolved commit — `git fetch <remote> <sha>:<dst>` can't fetch
by a bare sha (the left side of a refspec is a remote *ref
name*), so the resolution happens on hive-c0re's side. The
approval row stores both the manager-supplied sha and the
approval row stores both the submitted sha and the
canonical hive-c0re-vouched sha. From here on the proposed
repo is irrelevant for this approval — the manager can amend,
repo is irrelevant for this approval — the submitter can amend,
force-push, or `rm -rf` the proposed repo and the queued
approval still points at an immutable git object inside
applied.
@ -38,7 +41,7 @@ happens after a decision lands.
runs `nix flake lock` (no `--update-input` flags, so it only
fills missing entries), and rejects if the committed
`flake.lock` differs from the result. Triggered when the
manager added or removed `inputs` in `flake.nix` without
submitter added or removed `inputs` in `flake.nix` without
re-running `nix flake lock`. Fix: run `nix flake lock` in
the config repo, commit, and re-submit.
- **Duplicate inputs** — groups lock nodes by their canonical
@ -48,7 +51,7 @@ happens after a decision lands.
the `follows` directive, re-lock, and re-submit.
Both checks only flag *new* violations — agents whose lock
already carried duplicates before this check was added are
unaffected until a coordinated config-change pass via manager.
unaffected until a coordinated config-change pass.
4. Operator sees the proposal as a card on the dashboard — a
full multi-file diff, toggleable between three bases (vs the
running tree / vs the last approved proposal / vs the
@ -60,32 +63,36 @@ happens after a decision lands.
failure, main stays put and the working tree resets back to
the previous deployed commit.
6. `HelperEvent::ApprovalResolved` (and `Rebuilt` for the
ApplyCommit kind) land in the manager's inbox, carrying both
the canonical sha and the terminal tag.
ApplyCommit kind) land in the root agent's inbox, carrying both
the canonical sha and the terminal tag. (Helper events currently route
to the root agent via `notify_manager` regardless of which agent
submitted the approval; routing them to the submitting agent is tracked
in #1953.)
### Withdrawing a pending approval
The manager can call `cancel_loose_end(kind: "approval", id)` to
The submitting agent can call `cancel_loose_end(kind: "approval", id)` to
withdraw an approval that hasn't been acted on yet.
The row transitions to `ApprovalStatus::Cancelled` (distinct from
`Denied`/`Failed`), the dashboard pulls the card out of the
pending pane, and `ApprovalResolved { status: "cancelled" }` fires
on the manager + dashboard channels. Approvals that have already
on the root agent + dashboard channels. Approvals that have already
been approved/denied/failed return an error — the resolution is
final once the operator (or a lifecycle failure) acted on the row.
Sub-agent surface refuses the `approval` kind with a clear error:
sub-agents don't submit approvals, so they have nothing of their
own to withdraw. Manager-only.
The socket refuses the `approval` kind with a clear error for any
agent that lacks the `approvals` tool group: only an agent with that
group submits approvals (for its direct children), so an agent
without it has nothing of its own to withdraw.
`InitConfig` approvals are the first step in a two-step spawn
flow. On approve, hive-c0re seeds the proposed config repo with
a default `agent.nix` template and sends the manager
`HelperEvent::ConfigReady { agent }`. The manager then reviews,
a default `agent.nix` template and sends `HelperEvent::ConfigReady { agent }`
to the root agent. The submitting agent then reviews,
edits, and commits the template before calling `request_apply_commit`
to proceed to an `ApplyCommit` approval. The first `ApplyCommit`
creates the container; subsequent ones rebuild it with new config.
This gives the manager (and operator) an explicit review gate on the
This gives the submitting agent (and operator) an explicit review gate on the
initial configuration before any container is created.
### Approval kinds (wire shapes)
@ -94,7 +101,7 @@ initial configuration before any container is created.
`commit_ref` encoding because that field is overloaded as the
kind-specific payload carrier.
- `ApplyCommit``commit_ref` is the manager-supplied git sha
- `ApplyCommit``commit_ref` is the submitted git sha
(7-40 hex chars). The canonical, hive-c0re-vouched sha after the
proposal fetch lives in `fetched_sha` on the same `Approval`
row (only `ApplyCommit` populates it). See the End-to-end flow
@ -116,15 +123,15 @@ kind-specific payload carrier.
CLI). The host-level `HostRequest::Spawn` variant bypasses the
approval queue entirely — privileged-context use only (operator
on the host shell, test scripts, one-off recoveries). The
manager-side `RequestSpawn` is gone; managers go through the
agent-side `RequestSpawn` is gone; the submitting agent goes through the
`InitConfig``ApplyCommit` two-step instead so the spawn
captures their customised config.
captures the customised config.
- `InitConfig``commit_ref` is empty; the variant just gates
"seed the proposed repo with the default template" against
operator approval. Step 1 of the two-step spawn flow above.
- `UpdateMetaInputs``commit_ref` stores the JSON-encoded inputs
array (`"[]"` = all inputs, `"[\"nixpkgs\"]"` = just nixpkgs,
etc.). `agent` field is set to `ruth` (the requesting manager).
etc.). `agent` field is set to the requesting root agent.
On approve hive-c0re runs `nix flake update [inputs...]` on the
meta flake and commits the resulting lock changes.
- `SchedulePrompt``commit_ref` stores the JSON-encoded
@ -140,7 +147,7 @@ kind-specific payload carrier.
Two ways a row lands in `scheduled_prompts`:
- **Operator-direct** (`source = "operator"`): the operator adds a schedule through the dashboard form. Lands in the table immediately, no approval gate — operator action is already the trust boundary.
- **Agent-requested** (`source = "approval:<id>"`): a sub-agent (or the manager) submits a `RequestSchedulePrompt` through the manager socket. An `ApprovalKind::SchedulePrompt` row is queued; on approve, hive-c0re inserts the schedule row with `source = approval:<id>` so the audit trail points back at the operator decision (above).
- **Agent-requested** (`source = "approval:<id>"`): an agent submits a `RequestSchedulePrompt` through its MCP socket (the `request_schedule_prompt` tool, `scheduling` group). An `ApprovalKind::SchedulePrompt` row is queued; on approve, hive-c0re inserts the schedule row with `source = approval:<id>` so the audit trail points back at the operator decision (above).
No self-target shortcut: even agent-self schedules need approval. The existing `remind` MCP tool stays the quick self-wake path (no approval, lands directly in the agent's own inbox); this module is the bigger, multi-recipient, operator-visible thing.
@ -206,8 +213,10 @@ systemd drop-in, fails any pending approvals. Persistent state
**kept by default** — recreating the agent with the same name
reuses prior config + login. With `purge = true` the agent's
`/var/lib/hyperhive/{agents,applied}/<name>/` trees are also
wiped (config history + creds + notes gone forever). The manager
refuses to destroy itself.
wiped (config history + creds + notes gone forever). The
root/bootstrap container is destroyable like any other — hive-c0re
recreates it on the next startup if it's absent, so destroying it is
transient.
## Meta flake
@ -255,7 +264,7 @@ from the agent list; if it differs from disk, runs
`nix flake lock` + commits as `regenerate meta flake` (or
`seed meta from N agent(s)` on the very first call).
The manager has `/meta` RO-bound inside its container:
The root agent has `/meta` RO-bound inside its container:
`git -C /meta log --oneline` is the swarm-wide deploy log,
`cat /meta/flake.lock | jq '.nodes["agent-<n>"].locked'`
resolves which sha each agent is pinned at right now.
@ -265,15 +274,15 @@ per container row.
## Two repos per agent
```
/var/lib/hyperhive/agents/<name>/config/ proposed — manager RW
└── <anything> # any files the manager
# wants in the commit.
/var/lib/hyperhive/agents/<name>/config/ proposed — submitting agent RW
└── <anything> # any files the submitting
# agent wants in the commit.
# agent.nix is the
# convention entry
# point; flake.nix is
# tracked boilerplate
# (manager doesn't edit
# it).
# (submitting agent doesn't
# edit it).
/var/lib/hyperhive/applied/<name>/ applied — core-only
├── .git/ # tag-rich history
@ -281,7 +290,7 @@ per container row.
│ # boilerplate exporting
│ # nixosModules.default
├── agent.nix # working tree of main
└── <other manager files> # also tracked
└── <other committed files> # also tracked
/var/lib/hyperhive/meta/ swarm-wide flake — core
├── .git/ # one commit per successful
@ -290,7 +299,7 @@ per container row.
└── flake.lock # pins each agent's sha
```
Why two physical repos: the manager's `/agents/<n>/config/` is
Why two physical repos: the submitting agent's `/agents/<n>/config/` is
RW — a buggy or hostile agent can `git clean -fdx` its own
proposed tree. The applied repo is never bind-mounted (except
the read-only `.git` exposure described below) so a destructive
@ -318,7 +327,7 @@ underlying commit inside the applied repo:
| `denied/<id>` | operator deny | yes (body = operator note) |
`applied/main` is always the latest `deployed/*`. `denied/` and
`failed/` are terminal; the manager submits a new commit + new
`failed/` are terminal; the submitting agent submits a new commit + new
approval id to retry. Because tags are first-class git objects,
rejected and failed trees stay browsable forever — `git log
--tags` in the applied repo is the audit trail.
@ -384,29 +393,33 @@ The org is private and agents are not members, so only the
reach another agent's config — or even its own — through the
forge. The tokenised push URL is passed inline to `git push`,
never written into `applied/<n>/.git/config`; that repo is
RO-bind-mounted into the manager, and a stored token would leak
RO-bind-mounted into the root agent, and a stored token would leak
core's admin credential to an agent.
The dashboard deep-links into this org — a `config repo` link
per container row and a `commit on forge` link per approval
card. See `docs/web-ui.md`.
### Manager view of applied + meta
### Root-agent view of applied + meta
The manager container gets three host-side bind mounts via
The root agent container gets three host-side bind mounts via
`set_nspawn_flags`:
- `/var/lib/hyperhive/agents/``/agents/` (RW) — proposed
repos. Manager edits + commits per-agent config here.
repos. The root agent edits + commits per-agent config here.
- `/var/lib/hyperhive/applied/``/applied/` (RO) — every
agent's authoritative applied repo, including `.git`.
- `/var/lib/hyperhive/meta/``/meta/` (RO) — the swarm-wide
deploy flake.
This is the **root agent's** view — RW over *every* agent's config. An
agent with the `approvals` group that owns a subtree (a sub-manager) has
the equivalent RW scoped to its own children's config repos.
Each proposed repo (`/agents/<n>/config/`) is pre-configured
with `applied` as a git remote pointing at
`/applied/<n>/.git`. Useful incantations from inside the
manager:
root agent's container:
```sh
git -C /agents/<n>/config fetch applied
@ -420,7 +433,7 @@ git -C /meta log --oneline # swarm-wide deplo
cat /meta/flake.lock | jq '.nodes | with_entries(select(.key | startswith("agent-")))'
```
The RO binds block push at the kernel level, so the manager
The RO binds block push at the kernel level, so the root agent
can only fetch / read — git plumbing inside the container
cannot corrupt either authoritative repo.
@ -446,16 +459,16 @@ each phase is a no-op once already applied. Behaviour:
No state loss in either migration. claude creds, /state/
notes, the events DB, proposed history, and applied history
all survive. The manager keeps its session; sub-agents stay
all survive. The root agent keeps its session; sub-agents stay
logged in.
## Manager (`ruth`) is hive-c0re-managed
## The root/bootstrap container is hive-c0re-managed
The manager container runs through the **same lifecycle as
The root agent container runs through the **same lifecycle as
sub-agents**. On `hive-c0re serve` startup, if `ruth` is missing,
hive-c0re creates it. The manager's flake lives at
hive-c0re creates it. The root agent's flake lives at
`/var/lib/hyperhive/applied/ruth/`; its proposed config at
`/var/lib/hyperhive/agents/ruth/config/`. Manager can edit its own
`/var/lib/hyperhive/agents/ruth/config/`. The root agent can edit its own
`agent.nix` (visible inside the container at `/agents/ruth/config/`)
and submit `request_apply_commit("ruth", <sha>)` for operator
approval.
@ -467,29 +480,29 @@ Differences from sub-agents:
- Web UI port via `lifecycle::agent_web_port("ruth")` — same
FNV-1a hash as every other agent (8100..8999 range).
- `set_nspawn_flags` adds two extra binds: `/var/lib/hyperhive/agents`
`/agents` (RW) so the manager can edit per-agent proposed repos,
and `/var/lib/hyperhive/applied``/applied` (RO) so the manager
`/agents` (RW) so the root agent can edit per-agent proposed repos,
and `/var/lib/hyperhive/applied``/applied` (RO) so the root agent
can `git fetch` deployed/failed/denied tags from any agent's
authoritative applied repo (see "Manager view of applied" below).
- First-deploy spawn bypasses the approval queue (manager is
authoritative applied repo (see "Root-agent view of applied" below).
- First-deploy spawn bypasses the approval queue (the root agent is
required infrastructure).
- Per-agent socket lives at `/run/hyperhive/manager/`, owned by
`manager_server::start`.
**Migration note** (for older hosts): drop any `containers.root =
{ ... }` block from your host NixOS config. hyperhive creates and
updates the manager itself.
updates the root agent itself.
## Manager policy
## Root-agent policy
From `hive-ag3nt/prompts/system.md` (`<!-- role:manager -->` block,
rendered via `hive_ag3nt::prompt::render`): the manager does NOT
rendered via `hive_ag3nt::prompt::render`): the root agent does NOT
rubber-stamp sub-agent config requests. It verifies (role match,
package legitimacy, cheaper alternative, blast radius) before
committing and calling `request_apply_commit`.
For ambiguous cases or anything that needs human signal, the
manager calls `ask(question, options?, multi?, ttl_seconds?, to?)`
the root agent calls `ask(question, options?, multi?, ttl_seconds?, to?)`
queues the question and returns the id immediately. When `to` is
omitted (or `"operator"`) the question shows up on the dashboard;
when `to` is a sub-agent's name, the recipient receives a
@ -500,7 +513,7 @@ in the asker's inbox. Storage is `hive-c0re::operator_questions`
(sqlite) — same table, with a nullable `target` column
(NULL = operator). Dispatch goes through
`hive-c0re/src/questions.rs::{handle_ask, handle_answer}` so both
the agent + manager surfaces stay aligned. The answer flow is:
the agent + root-agent surfaces stay aligned. The answer flow is:
```
POST /answer-question/{id} agent: Answer { id, answer }
@ -513,19 +526,21 @@ POST /answer-question/{id} agent: Answer { id, answer }
Two more paths resolve a pending question with a sentinel answer:
- `POST /cancel-question/{id}` (✗ CANC3L button on the dashboard)
resolves with `[cancelled]`. The manager sees a terminal state
resolves with `[cancelled]`. The root agent sees a terminal state
and can fall back.
- `ttl_seconds` deadline: a tokio watchdog spawned at submit time
fires `answer(id, "[expired]")` once the ttl runs out. Already-
resolved races no-op. The dashboard surfaces a `⏳ MM:SS` chip
on each pending question with a deadline.
## Helper events to the manager
## Helper events to the root agent
`Coordinator::notify_manager(&HelperEvent)` enqueues an inbox
message from sender `system` with the event JSON in the body. The
manager harness no longer short-circuits these — they drive a
regular claude turn so the manager can react. Variants
root agent's harness no longer short-circuits these — they drive a
regular claude turn so the root agent can react. (Today these go to the
root agent regardless of which agent submitted the approval — routing to
the submitting agent is tracked in #1953.) Variants
(`hive_sh4re::HelperEvent`):
- `ApprovalResolved { id, agent, commit_ref, status, note }`
@ -537,7 +552,7 @@ regular claude turn so the manager can react. Variants
(covers startup scan + manual `/rebuild` from dashboard) +
`actions::approve` (ApplyCommit).
- `Killed { agent }` — admin `HostRequest::Kill` + dashboard
`/kill` + manager `Kill` MCP tool.
`/kill` + the `Kill` MCP tool.
- `Destroyed { agent }``actions::destroy`.
- `ContainerCrash { agent, note }``crash_watch`: a previously-
running container went away with no operator-initiated transient
@ -546,19 +561,19 @@ regular claude turn so the manager can react. Variants
tombstone, three `POLL_INTERVAL`s — closes the race where a
lifecycle op finishes between two crash-watch polls and the
container shows briefly as "stopped without transient" before
the next start). Manager can `start` it again or escalate.
the next start). The root agent can `start` it again or escalate.
- `NeedsLogin { agent }` — sub-agent has no claude session yet.
Manager can't act directly (interactive OAuth); typically flags
The root agent can't act directly (interactive OAuth); typically flags
the operator.
- `LoggedIn { agent }` — sub-agent just completed login. Manager
- `LoggedIn { agent }` — sub-agent just completed login. The root agent
often greets the agent on this event.
- `ConfigReady { agent }` — a new agent's proposed config repo was
just seeded (post-`InitConfig` approval). The manager can now
just seeded (post-`InitConfig` approval). The root agent can now
edit `/agents/<agent>/config/agent.nix`, commit the changes,
and submit `request_apply_commit` with the commit sha to create
the container (first ApplyCommit also triggers spawn bookkeeping).
- `NeedsUpdate { agent }` — sub-agent's recorded flake rev is
stale. Manager calls `update(name)` to rebuild — idempotent,
stale. The root agent calls `update(name)` to rebuild — idempotent,
no approval required.
- `QuestionAnswered { id, question, answer, answerer }`
dashboard `/answer-question/{id}` (answerer = `"operator"`),
@ -581,18 +596,18 @@ that don't change the deployed commit (e.g.
`auto_update::rebuild_agent` reapplying the existing main, or the
dashboard `↻ R3BU1LD` button when the lock didn't move). When set,
`git show <sha>` against `/agents/<n>/applied.git` inside the
manager container yields the exact tree that was referenced.
bootstrap container yields the exact tree that was referenced.
To add a new event: new `HelperEvent` variant + call sites + update
`prompts/system.md` (`<!-- role:manager -->` block, the lifecycle-
event list) so the manager knows the new shape.
event list) so the root agent knows the new shape.
## Auto-update on startup
`hive-c0re serve` runs `auto_update::run` in a background task right
after opening the coordinator. It enumerates managed containers and
rebuilds any whose recorded hyperhive rev differs from the current
one — sub-agents and manager go through the same `lifecycle::rebuild`
one — sub-agents and the root agent go through the same `lifecycle::rebuild`
path.
"Rev" = canonical filesystem path of `cfg.hyperhiveFlake`. Marker