diff --git a/docs/approvals.md b/docs/approvals.md index 2656eb0e..c32eb7bd 100644 --- a/docs/approvals.md +++ b/docs/approvals.md @@ -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//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//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 :` 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:"`): 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:` so the audit trail points back at the operator decision (above). +- **Agent-requested** (`source = "approval:"`): 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:` 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}//` 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-"].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//config/ proposed — manager RW -└── # any files the manager - # wants in the commit. +/var/lib/hyperhive/agents//config/ proposed — submitting agent RW +└── # 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// applied — core-only ├── .git/ # tag-rich history @@ -281,7 +290,7 @@ per container row. │ # boilerplate exporting │ # nixosModules.default ├── agent.nix # working tree of main -└── # also tracked +└── # 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//config/` is +Why two physical repos: the submitting agent's `/agents//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/` | 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//.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//config/`) is pre-configured with `applied` as a git remote pointing at `/applied//.git`. Useful incantations from inside the -manager: +root agent's container: ```sh git -C /agents//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", )` 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` (`` 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//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 ` against `/agents//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` (`` 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