Watch
0
0
Fork
You've already forked hyperhive
0

docs: the hive has no approval queue

config-changes.md (was approvals.md) describes config PRs, the meta flake
and helper events; the approval queue, its kinds, the withdraw path, the
job-queue dispatch and ApprovalResolved go. Every other page drops the
approvals table, endpoints, event, tool group, hivectl verbs, dashboard
section and the loose-end/cancel kind, and links to the renamed page.
This commit is contained in:
atlas 2026-10-04 23:55:14 +02:00
commit e4b170f8c6
18 changed files with 122 additions and 327 deletions

View file

@ -31,11 +31,11 @@ hand-maintained per-file tree drifts out of sync with the code.
user). `src/main.rs` is the `hive-c0re` binary — **daemon-only** user). `src/main.rs` is the `hive-c0re` binary — **daemon-only**
(`serve` + the periodic vacuum/sweep loops); the operator CLI lives in (`serve` + the periodic vacuum/sweep loops); the operator CLI lives in
the separate `hivectl` crate, which talks to the daemon over the host the separate `hivectl` crate, which talks to the daemon over the host
admin socket. Owns the sqlite broker, approval + reminder admin socket. Owns the sqlite broker, the reminder queue, the meta
queues, the meta flake, lifecycle (`nixos-container` flake, lifecycle (`nixos-container` shellouts), gateway / forge /
shellouts), gateway / forge / matrix wiring (agent forge and matrix matrix wiring (agent forge and matrix accounts are
accounts are `swarm-controller`'s), per-container stats, and the axum `swarm-controller`'s), per-container stats, and the axum operator
operator dashboard (`dashboard/`). Largest crate. dashboard (`dashboard/`). Largest crate.
- **`hivectl/`** — standalone operator CLI (`hivectl` binary). Talks to - **`hivectl/`** — standalone operator CLI (`hivectl` binary). Talks to
the `hive-c0re` daemon over the host admin socket (`hive-host-sock` the `hive-c0re` daemon over the host admin socket (`hive-host-sock`
wire types) — does NOT link `hive-c0re`. Full, always-current verb wire types) — does NOT link `hive-c0re`. Full, always-current verb
@ -128,7 +128,7 @@ hand-maintained per-file tree drifts out of sync with the code.
the process is; the actual claude session survives a daemon restart the process is; the actual claude session survives a daemon restart
independently (see `session.rs`'s module doc). independently (see `session.rs`'s module doc).
- **`hive-sh4re/`** — shared wire types (Agent / Manager request + - **`hive-sh4re/`** — shared wire types (Agent / Manager request +
response, `Message`, `Approval`, `HelperEvent`) used across the unix response, `Message`, `HelperEvent`) used across the unix
sockets. Host-admin-socket and hive-priv-socket wire types have been sockets. Host-admin-socket and hive-priv-socket wire types have been
split out into their own crates (below) so `hivectl` and `hive-priv` split out into their own crates (below) so `hivectl` and `hive-priv`
don't need to pull in the rest of `hive-sh4re`. don't need to pull in the rest of `hive-sh4re`.

View file

@ -61,7 +61,7 @@ the swarm — one of each, each on whichever host you choose
the hives — the substrate, as many as you like (deploy.hive-controller.enable) the hives — the substrate, as many as you like (deploy.hive-controller.enable)
│ │
├── hive-c0re runs the agent containers: lifecycle, approvals, broker, ├── hive-c0re runs the agent containers: lifecycle, broker,
│ auto-rebuild, the per-hive dashboard; unprivileged │ auto-rebuild, the per-hive dashboard; unprivileged
├── hive-priv root helper, socket-activated, does bind-mounts + nsenter ├── hive-priv root helper, socket-activated, does bind-mounts + nsenter
├── hive-gateway nginx + dnsmasq on :80/:443, fronts every agent's web UI ├── hive-gateway nginx + dnsmasq on :80/:443, fronts every agent's web UI
@ -180,7 +180,7 @@ newer channel breaks in ways hyperhive's CI doesn't catch.
- **`swarmctl`** — the swarm's CLI, on the swarm-controller host: create - **`swarmctl`** — the swarm's CLI, on the swarm-controller host: create
agents and mint their identities, manage SSO users, make forge admins. agents and mint their identities, manage SSO users, make forge admins.
→ [reference](docs/tools/swarmctl-cli.md) → [reference](docs/tools/swarmctl-cli.md)
- **per-hive** — each hive's dashboard (approvals, container state, - **per-hive** — each hive's dashboard (container state,
rebuild queue) and `hivectl` for host-level administration and rebuild queue) and `hivectl` for host-level administration and
container shells. → [`docs/web-ui/`](docs/web-ui/README.md) · container shells. → [`docs/web-ui/`](docs/web-ui/README.md) ·
[hivectl](docs/tools/hivectl.md) [hivectl](docs/tools/hivectl.md)
@ -197,7 +197,7 @@ pick the page for your task rather than reading front to back. Some
common entry points: common entry points:
- connecting hives into a swarm → [`swarm/`](docs/swarm/README.md) - connecting hives into a swarm → [`swarm/`](docs/swarm/README.md)
- how config changes flow → [`agent-lifecycle/approvals.md`](docs/agent-lifecycle/approvals.md) - how config changes flow → [`agent-lifecycle/config-changes.md`](docs/agent-lifecycle/config-changes.md)
- trust boundary + threat model → [`trust-boundary/`](docs/trust-boundary/boundary.md) - trust boundary + threat model → [`trust-boundary/`](docs/trust-boundary/boundary.md)
- what an agent's turn looks like → [`turn-loop/`](docs/turn-loop/README.md) - what an agent's turn looks like → [`turn-loop/`](docs/turn-loop/README.md)
- every `services.hyperhive.*` option → [options reference](https://hyperhive.darkest.space/options/) - every `services.hyperhive.*` option → [options reference](https://hyperhive.darkest.space/options/)

View file

@ -26,8 +26,8 @@ declarations.
## Agent lifecycle ## Agent lifecycle
- **How do config changes flow from manager to operator to container?** → - **How do config changes flow from manager to operator to container?** →
[`agent-lifecycle/approvals.md`](agent-lifecycle/approvals.md) (approval kinds, approval [`agent-lifecycle/config-changes.md`](agent-lifecycle/config-changes.md)
state machine, `flake.lock` validation). (config PRs, the meta flake, helper events).
- **What state survives destroy / purge / restart?** → - **What state survives destroy / purge / restart?** →
[`agent-lifecycle/persistence.md`](agent-lifecycle/persistence.md). [`agent-lifecycle/persistence.md`](agent-lifecycle/persistence.md).
- **Who can do what to whom — the agent roster and privilege?** → - **Who can do what to whom — the agent roster and privilege?** →

View file

@ -1,15 +1,8 @@
# Approvals + helper events # Config changes + helper events
The approval queue is where an agent asks the operator for something it Config changes are pull requests on the agent's config repo, which an
can't do itself. Nothing submits a new approval today: the one kind operator merges on the forge; that merge deploys them. Helper events are
left, a meta-input bump, only reads back from existing rows. Config how agents stay informed about lifecycle changes that need a reaction.
changes don't go through this queue: they're pull requests
on the agent's config repo, which an operator merges on the forge, and
that merge deploys them. 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 both; helper events are how
it stays informed about what happens after a decision lands.
## For operators ## For operators
@ -17,17 +10,6 @@ it stays informed about what happens after a decision lands.
config (or its own, via a sub-manager) as a pull request on config (or its own, via a sub-manager) as a pull request on
`agent-configs/<agent>`. Review it on the forge like any other PR, and `agent-configs/<agent>`. Review it on the forge like any other PR, and
merge it there to deploy it. See [Config changes](#config-changes). merge it there to deploy it. See [Config changes](#config-changes).
- **Meta/flake update** (`UpdateMetaInputs`) — an existing row reads
back and you can approve it. Approving runs the update and commits
the lock change; it doesn't rebuild anything by itself.
Don't want to approve something? **Deny it** (`DENY` on the dashboard
card, or `hivectl approvals deny <id>`) — nothing runs. Either way the
submitting agent is always notified that the operator denied their request; what's
optional is only the reason text, which you can add on the dashboard's
prompt (cancelling that prompt aborts the whole deny, not just the
reason) but not from the CLI. Denying is final: a denied approval
can't be re-approved later, the agent has to submit a fresh request.
Everything below this point is the implementation detail behind that Everything below this point is the implementation detail behind that
flow. flow.
@ -38,8 +20,7 @@ Config changes flow through a **forge pull request** on the agent's
`agent-configs/<name>` repo — the same surface agents use for code PRs. `agent-configs/<name>` repo — the same surface agents use for code PRs.
No MCP tool exists for config changes: opening the PR IS the request. No MCP tool exists for config changes: opening the PR IS the request.
1. The submitting agent (the child's parent, holding the `approvals` 1. The submitting agent **clones** `agent-configs/<name>`, edits it there (any
tool group) **clones** `agent-configs/<name>`, edits it there (any
tracked path, but `agent.nix` is the contract entry point), commits tracked path, but `agent.nix` is the contract entry point), commits
with its own git identity, and pushes a branch + opens a PR with with its own git identity, and pushes a branch + opens a PR with
`hive-forge` — the same way it would change any other repo. `hive-forge` — the same way it would change any other repo.
@ -77,46 +58,17 @@ failed rebuild shows on the hive like any other and isn't commented on
the PR. A merge whose webhook delivery never reaches the controller the PR. A merge whose webhook delivery never reaches the controller
deploys nothing. deploys nothing.
## Approval queue Creating a brand-new agent is swarm-level (`swarmctl agent create`,
`POST /api/agents` on the swarm controller), and no hive can originate
### Withdrawing a pending approval an agent. The swarm controller's `InitAgentConfigRepo` job seeds the
agent's config repo with a default `agent.nix` template, then asks the
The submitting agent can call `cancel_loose_end(kind: "approval", id)` to target hive to deploy it.
withdraw an approval the operator hasn't 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 root agent + dashboard channels. Approvals that the operator
(or a lifecycle failure) has already approved, denied, or failed return an error — the resolution is
final once acted on.
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.
Creating a brand-new agent isn't an approval: it's swarm-level
(`swarmctl agent create`, `POST /api/agents` on the swarm controller),
and no hive can originate an agent. The swarm controller's
`InitAgentConfigRepo` job seeds the agent's config repo with a default
`agent.nix` template, then asks the target hive to deploy it.
Changing what the template seeded isn't a special case: like every Changing what the template seeded isn't a special case: like every
later change, it's a PR on that config repo, made from a clone and later change, it's a PR on that config repo, made from a clone and
merged by an operator on the forge (see [Config changes](#config-changes)). merged by an operator on the forge (see [Config changes](#config-changes)).
### Approval kinds (wire shapes) ## Agent lifecycle
`ApprovalKind` carries one variant, and it overloads `commit_ref` as
its payload carrier.
<!-- vale write-good.Passive = NO -->
- `UpdateMetaInputs` — `commit_ref` stores the JSON-encoded inputs
array (`"[]"` = all inputs, `"[\"nixpkgs\"]"` = just nixpkgs,
etc.). hive-c0re sets the `agent` field to the requesting root agent.
On approve hive-c0re runs `nix flake update [inputs...]` on the
meta flake and commits the resulting lock changes.
<!-- vale write-good.Passive = YES -->
### Reminder delivery: file-path semantics ### Reminder delivery: file-path semantics
@ -144,10 +96,10 @@ body strings before calling it.
### Destroy semantics ### Destroy semantics
`HostRequest::Destroy { name, purge }` is the lifecycle tear-down, `HostRequest::Destroy { name, purge }` is the lifecycle tear-down.
not an approval. Stops + removes the nspawn container, drops the Stops + removes the nspawn container and drops the systemd drop-in.
systemd drop-in, fails any pending approvals. Persistent state Persistent state (proposed/applied repos, claude credentials, `/state/`
(proposed/applied repos, claude credentials, `/state/` notes) is notes) is
**kept by default** — recreating the agent with the same name **kept by default** — recreating the agent with the same name
reuses prior config + login. With `purge = true` the agent's reuses prior config + login. With `purge = true` the agent's
`/var/lib/hyperhive/{agents,applied}/<name>/` trees are also `/var/lib/hyperhive/{agents,applied}/<name>/` trees are also
@ -252,41 +204,6 @@ forge and meta's lock commits record it. A config PR nobody merges
carries no extra state on the forge side: the PR stays open, and carries no extra state on the forge side: the PR stays open, and
the submitter pushes again (or closes it) to retry. the submitter pushes again (or closes it) to retry.
### Dispatch via the job queue
Long-running approval work — `UpdateMetaInputs` — runs as a DAG on the
global job queue (`docs/scheduler/coordinator.md::Job queue`), submitted
by the approval handler rather than run inline:
| `ApprovalKind` | DAG submitted | source |
|---|---|---|
| `UpdateMetaInputs` | `meta_update` (`MetaLock` + rebuild fan-out) | `approval` |
The DAG carries the originating `approval_id`. **Every** queued kind
resolves through `actions::resolve_approval_dag` when its DAG settles
terminal: the DAG's own terminal state is the authoritative outcome.
That hook fires the matching `HelperEvent::*` via `finish_approval`.
Two visible consequences:
- **Operator dashboard**: after selecting APPR0VE the work-in-progress
shows up on the *rebuild queue* card (`GET /api/jobq/graph`, refetched
on every `rebuild_queue_changed` tick), not on the approvals panel
(which already moved the row to "approved"). A long meta-update
cascade renders as a parent DAG with one child rebuild per affected
agent — see `docs/web-ui/dashboard.md` for the layout.
- **Cancellation**: the dashboard's *× cancel* button on a still-queued
DAG calls `POST /api/rebuild-queue/{id}/cancel`, which flips it to
`Cancelled` before any node runs (and fails the approval row instead
of leaving it dangling). Returns `{"cancelled": true}` on success,
`{"cancelled": false}` once any node started — terminal states can't
be retroactively rewritten.
The `approval` source + `approval_id` mean a tail-end build failure
surfaces back as a failed approval row, not just a silent queue
entry. `manual` (dashboard ↻ R3BU1LD) and `auto_update` (boot
reconcile) DAGs use the same queue but skip the approval plumbing.
### Forge mirror ### Forge mirror
The bundled `hive-forge` container runs on the swarm's forge host The bundled `hive-forge` container runs on the swarm's forge host
@ -323,10 +240,10 @@ calls `bind_child_agent_dirs` for each entry in
current config — not an editing surface. An agent without the current config — not an editing surface. An agent without the
capability sees no other agent's config at all. capability sees no other agent's config at all.
An agent with the `approvals` tool group submits a change the same way An agent submits a change to another agent's config the same way it
it makes any other change: **clone the target agent's config repo from the makes any other change: **clone the target agent's config repo from the
forge into its own state dir, commit on a branch, open a PR**, and let forge into its own state dir, commit on a branch, open a PR**, and let
the operator review and approve it. By design, no second, mount-shaped the operator review and merge it. By design, no second, mount-shaped
path reaches the same file without the review. path reaches the same file without the review.
Agents holding the `manage_root_agent` capability (granted per agent in Agents holding the `manage_root_agent` capability (granted per agent in
@ -401,7 +318,7 @@ hive-c0re creates it. The root agent's flake lives at
`/var/lib/hyperhive/applied/ruth/`; its proposed config at `/var/lib/hyperhive/applied/ruth/`; its proposed config at
`/var/lib/hyperhive/agents/ruth/config/`. The root agent 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/`) `agent.nix` (visible inside the container at `/agents/ruth/config/`)
and open a config PR on `agent-configs/ruth` for operator approval, and open a config PR on `agent-configs/ruth` for operator review,
same as any other agent. same as any other agent.
Differences from sub-agents: Differences from sub-agents:
@ -415,8 +332,6 @@ Differences from sub-agents:
and `/var/lib/hyperhive/applied` → `/applied` (RO) so the root agent and `/var/lib/hyperhive/applied` → `/applied` (RO) so the root agent
can `git fetch` deployed/failed/denied tags from any agent's can `git fetch` deployed/failed/denied tags from any agent's
authoritative applied repo (see "Root-agent view of applied" below). authoritative applied repo (see "Root-agent view of applied" below).
- First-deploy spawn bypasses the approval queue (the root agent is
required infrastructure).
- `socket_server::start_manager` binds the root agent's socket, - `socket_server::start_manager` binds the root agent's socket,
pure transport with no dedicated helpers — it uses the same pure transport with no dedicated helpers — it uses the same
per-agent runtime dir as any other agent (`/run/hyperhive/agents/ruth/`), per-agent runtime dir as any other agent (`/run/hyperhive/agents/ruth/`),
@ -432,26 +347,19 @@ The system prompt (`hive-agent/prompts/system.md`, rendered by
`hive-agent/src/prompt.rs`) is the **same for every agent**; what `hive-agent/src/prompt.rs`) is the **same for every agent**; what
varies is which MCP tools it surfaces (gated by tool groups and varies is which MCP tools it surfaces (gated by tool groups and
capabilities in `agent.nix`). No `role:manager` block renders only capabilities in `agent.nix`). No `role:manager` block renders only
for the root agent. The root agent's approval-gating for the root agent. The root agent's role-specific
behaviour comes from its CLAUDE.md / agent-specific instructions, not behaviour comes from its CLAUDE.md / agent-specific instructions, not
the system prompt template. the system prompt template.
## Helper events to the submitting agent ## Helper events
`Coordinator::notify_submitter(approval_id, &HelperEvent)` routes the `Coordinator::notify_agent(agent, &HelperEvent)` pushes the event into
event to the agent that originally submitted the approval (looked up from the agent's inbox, and the harness delivers it as a regular `system`
the `submitter` column on the `approvals` table). The harness delivers it inbox message so it drives a normal claude turn. A "FYI, check when
as a regular `system` inbox message so it drives a normal claude turn. convenient" event doesn't need a message — hive-c0re publishes those as
`finish_approval` fires an `ApprovalResolved` HelperEvent this way for swarm lifecycle notices instead (`swarm_notices::notify`). Variants
**every** approval kind's terminal state. A (`hive_sh4re::manager::HelperEvent`):
"FYI, check when convenient" event doesn't need a message — hive-c0re publishes those as swarm
lifecycle notices instead (`swarm_notices::notify`). Legacy
approval rows that predate the submitter column fall back to the
root agent. Variants (`hive_sh4re::manager::HelperEvent`):
- `ApprovalResolved { id, agent, commit_ref, status, note }` —
fired by `actions::approve` + `actions::deny` whenever an
approval transitions to its terminal state.
- `ContainerCrash { agent, note }` — `crash_watch`: a previously- - `ContainerCrash { agent, note }` — `crash_watch`: a previously-
running container went away with no operator-initiated transient running container went away with no operator-initiated transient
state (Stopping / Restarting / Destroying / Rebuilding) AND nothing state (Stopping / Restarting / Destroying / Rebuilding) AND nothing

View file

@ -20,8 +20,6 @@ keeps its state, purging it doesn't.**
Beyond that: Beyond that:
- **hive-c0re keeps approvals forever** — they're an audit trail, not a
cache. Nothing about them ever ages out.
- **Broker messages**: acked ones vacuum after 30 days; anything - **Broker messages**: acked ones vacuum after 30 days; anything
undelivered or delivered-but-not-yet-acked is always kept, however undelivered or delivered-but-not-yet-acked is always kept, however
old. old.
@ -40,8 +38,8 @@ schemas, file layouts, and internal migration mechanics.
### `/var/lib/hyperhive/db/broker.sqlite` (host) ### `/var/lib/hyperhive/db/broker.sqlite` (host)
Six tables, all in one file — two queues, a small key/value table, Five tables, all in one file — the message queue, a small key/value
the read-only schedule header/targets split, and the per-agent table, the read-only schedule header/targets split, and the per-agent
power-intent registry: power-intent registry:
- `messages` — every inter-agent / operator-bound message. - `messages` — every inter-agent / operator-bound message.
@ -56,8 +54,6 @@ power-intent registry:
per-agent store — see [`/harness/` contents per-agent store — see [`/harness/` contents
below](#state-dirs-per-agent) for where reminders (and todos) below](#state-dirs-per-agent) for where reminders (and todos)
live. live.
- `approvals` — the queue. `agent / kind (update_meta_inputs) /
commit_ref / requested_at / status / resolved_at / note`.
- `scheduled_prompts` — the scheduled prompts left from before they - `scheduled_prompts` — the scheduled prompts left from before they
moved to swarm level ([`swarm/schedules.md`](../swarm/schedules.md)). moved to swarm level ([`swarm/schedules.md`](../swarm/schedules.md)).
`owner / body / interval_seconds (NULL = one-shot) / `owner / body / interval_seconds (NULL = one-shot) /
@ -86,8 +82,6 @@ Retention:
rows are always kept — the harness `ack_turn`s only after a rows are always kept — the harness `ack_turn`s only after a
successful turn, so `requeue_inflight` can still requeue successful turn, so `requeue_inflight` can still requeue
an unacked row on a crash. an unacked row on a crash.
- hive-c0re keeps approvals indefinitely — an audit trail. `actions::destroy`
rows stay visible to anything that queries by id.
- Scheduled prompts: nothing deletes them; the rows stay as they are. - Scheduled prompts: nothing deletes them; the rows stay as they are.
- `agent_power` rows live until the operator destroys the agent (one row per - `agent_power` rows live until the operator destroys the agent (one row per
agent — nothing to vacuum). agent — nothing to vacuum).
@ -362,10 +356,10 @@ unless it holds the capability, nothing else.
Under `/var/lib/hyperhive/applied/<name>/` — the hive-c0re-only Under `/var/lib/hyperhive/applied/<name>/` — the hive-c0re-only
applied repo. Tracks `flake.nix` (module-only boilerplate; never applied repo. Tracks `flake.nix` (module-only boilerplate; never
edited after first spawn) + `agent.nix` (the actual config; the edited after first spawn) + `agent.nix` (the actual config; edits land
root agent's edits land here via the approval flow) + any other here from merged config PRs) + any other files those PRs commit.
files committed via the approval flow. `.git/` carries the proposal / `.git/` carries the proposal / approved / building / deployed / failed /
approved / building / deployed / failed / denied tag history. denied tag history.
Under `/var/lib/hyperhive/meta/` — the swarm-wide deploy flake plus Under `/var/lib/hyperhive/meta/` — the swarm-wide deploy flake plus
system-level config files. Single git repo for the whole host; hive-c0re system-level config files. Single git repo for the whole host; hive-c0re
@ -411,18 +405,17 @@ hive-c0re renders containers onto `meta#<n>` at creation.
See [For operators](#for-operators) above for what each action does to See [For operators](#for-operators) above for what each action does to
an agent's state. The mechanics, for completeness: an agent's state. The mechanics, for completeness:
- `DESTR0Y` also drops the systemd drop-in and fails any pending - `DESTR0Y` also drops the systemd drop-in; reviving the agent is
approvals; reviving the agent is `swarmctl agent create` with the same `swarmctl agent create` with the same name and hive, which reuses the
name and hive, which reuses the kept state. kept state.
- `PURG3` wipes `/var/lib/hyperhive/{agents,applied}/<name>/` — the - `PURG3` wipes `/var/lib/hyperhive/{agents,applied}/<name>/` — the
union of everything `DESTR0Y` left behind. union of everything `DESTR0Y` left behind.
`actions::destroy` implements the root/bootstrap agent's specialness as a soft policy `actions::destroy` implements the root/bootstrap agent's specialness as a soft policy
guard that refuses to destroy it, backstopped by guard that refuses to destroy it, backstopped by
`auto_update::ensure_root_agent`, which recreates it on the next `auto_update::ensure_root_agent`, which recreates it on the next
hive-c0re startup if it's ever absent (bypassing the approval queue, hive-c0re startup if it's ever absent — so even without the guard,
as required infrastructure) — so even without the guard, destroying it destroying it would only be transient.
would only be transient.
### btrfs subvolumes for `/var/lib/hyperhive/agents/<name>` ### btrfs subvolumes for `/var/lib/hyperhive/agents/<name>`

View file

@ -129,7 +129,7 @@ swarm UI's job view for progress.
Later config changes are PRs on `agent-configs/iris`; you merge them on the Later config changes are PRs on `agent-configs/iris`; you merge them on the
forge, and the merge deploys them. → forge, and the merge deploys them. →
[`agent-lifecycle/approvals.md`](../agent-lifecycle/approvals.md#config-changes) [`agent-lifecycle/config-changes.md`](../agent-lifecycle/config-changes.md#config-changes)
## Optional · Lock the hive dashboard ## Optional · Lock the hive dashboard
@ -150,7 +150,6 @@ hivectl list-agents # this hive's agents, status, rev
hivectl agent <name> restart # stop + start, no rebuild hivectl agent <name> restart # stop + start, no rebuild
hivectl agent <name> watch # follow its live event stream hivectl agent <name> watch # follow its live event stream
hivectl agent <name> choom # interactive claude session in its container hivectl agent <name> choom # interactive claude session in its container
hivectl approvals pending # what's waiting on you
hivectl open # this hive's dashboard (or: forge, matrix) hivectl open # this hive's dashboard (or: forge, matrix)
``` ```

View file

@ -67,7 +67,7 @@ Two things live in the `agent-configs` Forgejo organization:
allowlists are the `operators` team, and the agent can neither push allowlists are the `operators` team, and the agent can neither push
`main` directly nor self-merge. An operator's merge in the Forgejo UI `main` directly nor self-merge. An operator's merge in the Forgejo UI
deploys the merged commit (see deploys the merged commit (see
[approvals.md § Config changes](../agent-lifecycle/approvals.md#config-changes)). [config-changes.md § Config changes](../agent-lifecycle/config-changes.md#config-changes)).
hive-c0re never force-pushes: the `push_config` mirror pushes the hive-c0re never force-pushes: the `push_config` mirror pushes the
add-only status tags and `main` without force, and treats a refused add-only status tags and `main` without force, and treats a refused
`main` push as expected. `main` push as expected.
@ -75,7 +75,7 @@ Two things live in the `agent-configs` Forgejo organization:
agent's config. (Agents remain read-only collaborators on `core/meta`.) agent's config. (Agents remain read-only collaborators on `core/meta`.)
hive-c0re also references this repo as the agent's **persistent meta hive-c0re also references this repo as the agent's **persistent meta
flake input** (`agent-<n>.url = git+http://<forge>/agent-configs/<n>.git`; flake input** (`agent-<n>.url = git+http://<forge>/agent-configs/<n>.git`;
see [approvals.md § Meta flake](../agent-lifecycle/approvals.md)), fetching it as the `core` see [config-changes.md § Meta flake](../agent-lifecycle/config-changes.md#meta-flake)), fetching it as the `core`
user via a git credential helper that reads the live forge-core token — user via a git credential helper that reads the live forge-core token —
so the config lives on the forge, not a hand-synced local checkout. so the config lives on the forge, not a hand-synced local checkout.
- The dashboard links each container's "config" anchor to this - The dashboard links each container's "config" anchor to this

View file

@ -141,7 +141,7 @@ A failed or missing login gets `401` with a styled `unauthorized.html` naming th
nginx is the one external entry point. The per-agent web-port range (`8100`–`8999`) stays closed: agents serve their UI on a unix socket ([Per-agent unix-socket upstream](#per-agent-unix-socket-upstream)), and the hashed TCP port (`lifecycle::agent_web_port`) is only a fallback bind for an agent missing `HIVE_WEB_SOCKET` — the gateway never proxies through it. nginx is the one external entry point. The per-agent web-port range (`8100`–`8999`) stays closed: agents serve their UI on a unix socket ([Per-agent unix-socket upstream](#per-agent-unix-socket-upstream)), and the hashed TCP port (`lifecycle::agent_web_port`) is only a fallback bind for an agent missing `HIVE_WEB_SOCKET` — the gateway never proxies through it.
The dashboard port (`services.hyperhive.c0re.dashboardPort`, default 7000) binds `127.0.0.1` only, so remote dashboard access goes through the gateway. The dashboard can approve, deny and destroy; don't expose it without a reverse proxy in front. The dashboard port (`services.hyperhive.c0re.dashboardPort`, default 7000) binds `127.0.0.1` only, so remote dashboard access goes through the gateway. The dashboard can stop, rebuild and destroy agents; don't expose it without a reverse proxy in front.
## `HIVE_FORGE_URL`: agents reach the forge via the gateway by domain ## `HIVE_FORGE_URL`: agents reach the forge via the gateway by domain

View file

@ -163,27 +163,18 @@ the dashboard's "mark all read" (unbounded, per-agent).
### Loose-ends wire shape ### Loose-ends wire shape
`LooseEnd` is the per-row response shape for `GetLooseEnds` — one `LooseEnd` is the per-row response shape for `GetLooseEnds` — one
request shape, no per-socket flavours. Tagged enum so request shape, no per-socket flavours. Tagged enum so new thread kinds
new thread kinds (forge PRs, long-running approvals from a (forge PRs, etc.) can land later without breaking existing handlers.
privileged bot, etc.) can land later without breaking existing Each row carries enough context that the caller renders
handlers. Each row carries enough context that the caller renders
it directly as a bulleted list, no follow-up fetch needed. it directly as a bulleted list, no follow-up fetch needed.
`GetLooseEnds` takes no target — it always returns the caller's own `GetLooseEnds` takes no target — it always returns the caller's own
rows, on both the agent and manager sockets alike (the manager, `ruth`, rows, on both the agent and manager sockets alike (the manager, `ruth`,
is a normal agent here, just deployed automatically with different is a normal agent here, just deployed automatically with different
default capabilities). `Approval` rows only appear when the calling agent is default capabilities). `Reminder` rows always carry `owner == self`.
the manager (sub-agents don't submit approvals). `Reminder` rows always
carry `owner == self`.
Per-variant fields: Per-variant fields:
- `Approval { id, agent, commit_ref, description?, age_seconds }`
— `agent` is the affected agent (target of the spawn / config
commit), not the asker. `description` is the manager's free-text
blurb shown on the dashboard card. `commit_ref` is the
kind-specific payload (see `docs/agent-lifecycle/approvals.md::Approval kinds
(wire shapes)`).
- `Reminder { id, owner, message, due_at, age_seconds }` — - `Reminder { id, owner, message, due_at, age_seconds }` —
`due_at` is the absolute time the scheduler is targeting (RFC `due_at` is the absolute time the scheduler is targeting (RFC
3339 on the wire, see *Timestamps on the wire* below); clients 3339 on the wire, see *Timestamps on the wire* below); clients
@ -205,16 +196,9 @@ Per-variant fields:
unsynchronised wall clock, etc.) so the bulleted list never unsynchronised wall clock, etc.) so the bulleted list never
shows nonsense ages. shows nonsense ages.
`CancelLooseEnd { kind, id }` is the matching write surface. The `cancel_loose_end` is the matching write surface, handled in the
`kind` enum (`Reminder` / `Approval`) selects which underlying store harness without a hive-c0re round-trip: `reminder` deletes one of the
the dispatcher reaches into. `Reminder` cancels from either surface caller's own pending reminders, `todo` clears one of its todos.
subject to an ownership check (the scheduling agent).
`Approval` is manager-only — sub-agents don't submit approvals
so they have nothing of their own to withdraw; their wire
surface returns a clear error if they try. Cancelling an approval
transitions the row to `ApprovalStatus::Cancelled` and fires
`ApprovalResolved { status: "cancelled" }` so the dashboard pulls
the card out of the pending pane.
### Agent metadata ### Agent metadata
@ -296,7 +280,6 @@ binary flavor.
| `inbox` | `get_loose_ends`, `cancel_loose_end`, `remind` | | `inbox` | `get_loose_ends`, `cancel_loose_end`, `remind` |
| `execution` | vestigial — `mcp__bash__run` / `mcp__bash__status` are always available unconditionally via `extraMcpServers`; this group's entries expand to non-existent `mcp__hyperhive__run` / `mcp__hyperhive__status` and have no effect. See `docs/tools/bash.md`. | | `execution` | vestigial — `mcp__bash__run` / `mcp__bash__status` are always available unconditionally via `extraMcpServers`; this group's entries expand to non-existent `mcp__hyperhive__run` / `mcp__hyperhive__status` and have no effect. See `docs/tools/bash.md`. |
| `lifecycle` | none | | `lifecycle` | none |
| `approvals` | none — gates `cancel_loose_end`'s approval-cancel arm server-side. |
| `forge` | none | | `forge` | none |
| `web_tools` | none (gates the Claude built-ins `WebFetch`/`WebSearch`, not an MCP tool) | | `web_tools` | none (gates the Claude built-ins `WebFetch`/`WebSearch`, not an MCP tool) |
@ -316,7 +299,7 @@ clear them.
meta repo alongside `topology.json`). Format: `{ "alice": ["messaging", "meta", meta repo alongside `topology.json`). Format: `{ "alice": ["messaging", "meta",
"inbox", "lifecycle"], "bob": ["messaging", "meta", "inbox"] }`. An absent entry "inbox", "lifecycle"], "bob": ["messaging", "meta", "inbox"] }`. An absent entry
means "use role default." Tool permissions are intentionally NOT configurable means "use role default." Tool permissions are intentionally NOT configurable
from `agent.nix` — that file goes through the manager's approval flow, so from `agent.nix` — that file changes through config PRs the manager opens, so
letting it declare its own groups would let the manager grant itself any tool by letting it declare its own groups would let the manager grant itself any tool by
submitting a config commit, bypassing the operator gate. submitting a config commit, bypassing the operator gate.
@ -385,9 +368,7 @@ dashboard and `app.js` on the per-agent UI) intercepts, shows a spinner,
POSTs `application/x-www-form-urlencoded` (axum's `Form` extractor POSTs `application/x-www-form-urlencoded` (axum's `Form` extractor
rejects multipart), calls `refreshState()` on success. New mutating rejects multipart), calls `refreshState()` on success. New mutating
forms should add `data-async` and optionally `data-confirm` (for a forms should add `data-async` and optionally `data-confirm` (for a
JS-side `confirm()` prompt) or `data-prompt="…"` (for a JS-side `confirm()` prompt).
`window.prompt()` whose answer goes into a hidden input named by
`data-prompt-field`, default `note`).
`refreshState` defers automatically when `document.activeElement` `refreshState` defers automatically when `document.activeElement`
sits inside a managed section so the operator's typing isn't lost; sits inside a managed section so the operator's typing isn't lost;
@ -404,7 +385,7 @@ into the `Swap` node, then runs `nixos-container update` + stop +
start across the `StopForUpdate → Swap → RebuildBookkeeping` start across the `StopForUpdate → Swap → RebuildBookkeeping`
brace and the tail `Reconcile` node. `flake.nix` itself lives in the agent's brace and the tail `Reconcile` node. `flake.nix` itself lives in the agent's
proposed/applied repos and rides along on every fetch (see proposed/applied repos and rides along on every fetch (see
`docs/agent-lifecycle/approvals.md::Two repos per agent`). `docs/agent-lifecycle/config-changes.md::Two repos per agent`).
Anything that changes per-container state on the host should be Anything that changes per-container state on the host should be
re-applied here so a manual `↻ R3BU1LD` from the dashboard is re-applied here so a manual `↻ R3BU1LD` from the dashboard is
@ -412,7 +393,7 @@ sufficient to recover.
## Actions live in one place ## Actions live in one place
`approve` / `deny` / `destroy` (and the lifecycle helper) live in `destroy` (and the lifecycle helper) live in
`actions.rs` / `hive-c0re/src/dashboard/`. The admin socket and the dashboard `actions.rs` / `hive-c0re/src/dashboard/`. The admin socket and the dashboard
POST handlers both call into them so the two surfaces never drift. POST handlers both call into them so the two surfaces never drift.

View file

@ -218,15 +218,6 @@ overlap" case. REUSEADDR does **not** allow two simultaneous
`LISTEN` sockets on the same port (that would be `SO_REUSEPORT`, `LISTEN` sockets on the same port (that would be `SO_REUSEPORT`,
not used here) — it preserves exclusivity. not used here) — it preserves exclusivity.
## Approvals
### Orphan approvals
If test scripts or a manual `rm -rf` wipe state dirs out from under a
pending approval, the dashboard's next render marks them
`failed` with note `"agent state dir missing"` so they fall out of
`pending`. They stay in sqlite for audit.
## Gateway / SPA serving ## Gateway / SPA serving
### SPA fallback: use `Accept` header map, not `try_files ... /index.html` ### SPA fallback: use `Accept` header map, not `try_files ... /index.html`

View file

@ -24,7 +24,7 @@ from the same primitive nodes.
### Two levels: DAG and node ### Two levels: DAG and node
The **DAG** is the unit of cancel / approval-resolution and the The **DAG** is the unit of cancel and the
dashboard group; the **node** is the unit of scheduling / execution / dashboard group; the **node** is the unit of scheduling / execution /
build-log. Deps are intra-DAG edges only (`AfterOk` by default: build-log. Deps are intra-DAG edges only (`AfterOk` by default:
the dep must succeed, a failed/cancelled dep cancels the dependent — the dep must succeed, a failed/cancelled dep cancels the dependent —
@ -69,9 +69,8 @@ Cheap — no build slot:
| `PauseDrain` | await the harness reporting `PauseAcknowledged`, bounded timeout; best-effort like `Drain` | | `PauseDrain` | await the harness reporting `PauseAcknowledged`, bounded timeout; best-effort like `Drain` |
| `DestroyContainer` | `nixos-container destroy` + un-registration (drop from the roster, clear the ephemeral runtime dir). Runs downstream of a `Stop`, so deliberately excluded from `takes_container_down` — the container is already down by the time it claims | | `DestroyContainer` | `nixos-container destroy` + un-registration (drop from the roster, clear the ephemeral runtime dir). Runs downstream of a `Stop`, so deliberately excluded from `takes_container_down` — the container is already down by the time it claims |
| `PurgeState` | the `purge = true` half of a destroy: delete the agent's state subvolume (via hive-priv) plus its state/applied dirs. Own node because it's conditional and the irreversible step | | `PurgeState` | the `purge = true` half of a destroy: delete the agent's state subvolume (via hive-priv) plus its state/applied dirs. Own node because it's conditional and the irreversible step |
| `DestroyBookkeeping` | the post-destroy tail — meta sync, fail pending approvals, drop the power intent, notify the manager, rescan, re-emit the tombstone. Same split rationale as `RebuildBookkeeping`/`Swap`. Its `purge` flag only selects the wording of the approval-failure reason and the manager notification — the destructive work is `PurgeState`'s | | `DestroyBookkeeping` | the post-destroy tail — meta sync, drop the power intent, notify the manager, rescan, re-emit the tombstone. Same split rationale as `RebuildBookkeeping`/`Swap`. The destructive work is `PurgeState`'s |
| `SetWanted` | write the durable power intent (`wanted = Up`/`Offline`) as the head node of a power-op DAG. Takes the agent lease even though it's a store write, so the intent write and the tail `Reconcile` are atomic per-agent — two racing power ops can't clobber each other's intent before either reconciles | | `SetWanted` | write the durable power intent (`wanted = Up`/`Offline`) as the head node of a power-op DAG. Takes the agent lease even though it's a store write, so the intent write and the tail `Reconcile` are atomic per-agent — two racing power ops can't clobber each other's intent before either reconciles |
| `ResolveApproval` | tail of an approval-carrying DAG — resolve the approval row from how the work ended (`AfterAny`, one node emitted per outcome). Agentless: the approval row already names its agent |
| `EmitRebuilt` | tail of a rebuild/perm-change — emit the agent's `Rebuilt` manager event (ok/fail per outcome, nothing on cancel). One node per agent _and_ per outcome | | `EmitRebuilt` | tail of a rebuild/perm-change — emit the agent's `Rebuilt` manager event (ok/fail per outcome, nothing on cancel). One node per agent _and_ per outcome |
| `WriteDropin` | `set_nspawn_flags` + `set_resource_limits` + daemon-reload | | `WriteDropin` | `set_nspawn_flags` + `set_resource_limits` + daemon-reload |
| `WritePermFile` | commit `tool-groups.json` / `capabilities.json` (single git commit under `META_LOCK`) + emit the P3RM1SS10NS snapshots | | `WritePermFile` | commit `tool-groups.json` / `capabilities.json` (single git commit under `META_LOCK`) + emit the P3RM1SS10NS snapshots |
@ -139,7 +138,7 @@ stop(a..): online a: SetWanted(a,Off) → [Signal→Drain→ if graceful] Re
restart(a..): online a: [Signal→Drain→ if graceful] StopForUpdate(a) → Reconcile(a) (no SetWanted) restart(a..): online a: [Signal→Drain→ if graceful] StopForUpdate(a) → Reconcile(a) (no SetWanted)
offline a: Reconcile(a) (nothing to stop; Reconcile converges to existing wanted) offline a: Reconcile(a) (nothing to stop; Reconcile converges to existing wanted)
start(a..): a: SetWanted(a,Up) → Reconcile(a) (down+stale ⇒ SetWanted(a,Up) → «rebuild subgraph») start(a..): a: SetWanted(a,Up) → Reconcile(a) (down+stale ⇒ SetWanted(a,Up) → «rebuild subgraph»)
spawn(a): [wanted=Up at approve] Provision(a) → Create(a) → WriteDropin(a) → Reconcile(a) spawn(a): [wanted=Up at deploy] Provision(a) → Create(a) → WriteDropin(a) → Reconcile(a)
perm-change(a): WritePermFile(a) → «rebuild subgraph» perm-change(a): WritePermFile(a) → «rebuild subgraph»
meta-update(inp): MetaLock(inp) →(in-DAG) «rebuild subgraph» per affected agent meta-update(inp): MetaLock(inp) →(in-DAG) «rebuild subgraph» per affected agent
boot: (if any rev marker stale) MetaLock(hyperhive) →(in-DAG) «rebuild subgraph» per stale agent; boot: (if any rev marker stale) MetaLock(hyperhive) →(in-DAG) «rebuild subgraph» per stale agent;
@ -280,13 +279,6 @@ Roll-up state: `Failed` if any node failed, else `Running` / `Queued` /
DAGs — a flat cap over the whole sorted list, not per template, since DAGs — a flat cap over the whole sorted list, not per template, since
the dashboard renders one recent-builds list and one number bounds it. the dashboard renders one recent-builds list and one number bounds it.
### Approvals
`UpdateMetaInputs` approvals map onto the ordinary `meta-update` shapes.
The scheduler fires `actions::resolve_approval_dag` exactly once when
**any** approval-carrying DAG settles terminal (including
cancelled-while-queued, which fails the approval instead of dangling it).
### Wire shape ### Wire shape
`RebuildQueueChanged { seq }` (event name kept) — **a bare trigger, no `RebuildQueueChanged { seq }` (event name kept) — **a bare trigger, no
@ -297,7 +289,7 @@ That endpoint serves the graph generically (`hive-jobq-wire`): every node
carries `id`, `parent`, `deps`, `state`, `label` (the node kind's own wire carries `id`, `parent`, `deps`, `state`, `label` (the node kind's own wire
string — the kind _is_ the phase label, there is no separate sub-step string — the kind _is_ the phase label, there is no separate sub-step
string) and free-form `data` for what only some kinds have (`agent`, string) and free-form `data` for what only some kinds have (`agent`,
`approval_id`, `inputs`, `build_log_id`). Group roots ride as ordinary `inputs`, `build_log_id`). Group roots ride as ordinary
nodes, so a group's state is just the root's own `state`. nodes, so a group's state is just the root's own `state`.
No group-level `agent` exists — agent is per-node, so one group can No group-level `agent` exists — agent is per-node, so one group can
@ -521,7 +513,7 @@ which is almost never desired. Leave off on non-x86 hosts.
## See also ## See also
- `docs/agent-lifecycle/approvals.md` — approval flow - `docs/agent-lifecycle/config-changes.md` — config PRs, meta flake, helper events
- `docs/agent-lifecycle/persistence.md` — SQLite schema, state-dir layout - `docs/agent-lifecycle/persistence.md` — SQLite schema, state-dir layout
- `docs/process/conventions.md` — wire protocol, recipient sentinels - `docs/process/conventions.md` — wire protocol, recipient sentinels
- `docs/agent-lifecycle/agent-roster.md` — the agent roster and privilege - `docs/agent-lifecycle/agent-roster.md` — the agent roster and privilege

View file

@ -292,7 +292,7 @@ decision, not an event to adjudicate.
A `config-pr` delivery reporting a PR merged into `main` queues a deploy A `config-pr` delivery reporting a PR merged into `main` queues a deploy
of its `merge_commit_sha` on the one hive whose wanted state places the of its `merge_commit_sha` on the one hive whose wanted state places the
agent; with no such hive, or several, the controller deploys nothing. See agent; with no such hive, or several, the controller deploys nothing. See
[approvals.md § Config changes](../agent-lifecycle/approvals.md#config-changes). [config-changes.md § Config changes](../agent-lifecycle/config-changes.md#config-changes).
A merge whose `closed` delivery never arrives deploys nothing either; the A merge whose `closed` delivery never arrives deploys nothing either; the
operator recovers by redelivering that delivery from the forge hook page, operator recovers by redelivering that delivery from the forge hook page,
which re-enters the same handler and re-queues the deploy. which re-enters the same handler and re-queues the deploy.

View file

@ -18,8 +18,8 @@ always on), the gateway proxies all operator-facing traffic, and
- **Operator** — reaches every UI (the dashboard + every - **Operator** — reaches every UI (the dashboard + every
per-agent page) through the gateway, on one origin. per-agent page) through the gateway, on one origin.
The core daemon serves operator-authority actions (approve / deny, The core daemon serves operator-authority actions
answer-as-operator, lifecycle POSTs), reachable only via the (answer-as-operator, lifecycle POSTs), reachable only via the
gateway. gateway.
- **Agent** — speaks only for itself, only over its per-agent - **Agent** — speaks only for itself, only over its per-agent
unix socket. The socket's identity _is_ the agent (see unix socket. The socket's identity _is_ the agent (see

View file

@ -118,8 +118,9 @@ checkpoints**, not about sandboxing the agent from its own tools:
settings nor merge its own PR. It's **not** set up for external VCS (GitHub settings nor merge its own PR. It's **not** set up for external VCS (GitHub
etc.), though — there, operator-merge is process + accepted risk, not a etc.), though — there, operator-merge is process + accepted risk, not a
technical control. technical control.
- **Approvals** — config changes are config PRs an operator merges on the - **Operator sign-off** — config changes are config PRs an operator merges
forge (see [`approvals.md`](../agent-lifecycle/approvals.md)), and on the forge (see
[`config-changes.md`](../agent-lifecycle/config-changes.md)), and
scheduled prompts are operator-only, set at swarm level scheduled prompts are operator-only, set at swarm level
(see [`schedules.md`](../swarm/schedules.md)). (see [`schedules.md`](../swarm/schedules.md)).
@ -314,6 +315,6 @@ writes to only the paths `hive-priv` legitimately needs.
| Component | Runs as | Privilege needed for | | Component | Runs as | Privilege needed for |
| ---------------------------- | -------------- | ------------------------------------------------------------ | | ---------------------------- | -------------- | ------------------------------------------------------------ |
| `hive-c0re` | `hive-core` | broker, HTTP dashboard, approvals | | `hive-c0re` | `hive-core` | broker, HTTP dashboard |
| `hive-priv` | `root` | container lifecycle, journal reads, bind mounts, cred writes | | `hive-priv` | `root` | container lifecycle, journal reads, bind mounts, cred writes |
| `hive-ag3nt` (per-container) | per-agent user | turn execution, MCP serving | | `hive-ag3nt` (per-container) | per-agent user | turn execution, MCP serving |

View file

@ -24,8 +24,8 @@ for what reaches a subagent's own `--mcp-config` and what doesn't.
Tool groups (`HIVE_TOOL_GROUPS`) gate tool access. The default Tool groups (`HIVE_TOOL_GROUPS`) gate tool access. The default
preset (`AGENT_DEFAULT`) includes `messaging`, `meta`, `inbox`, and preset (`AGENT_DEFAULT`) includes `messaging`, `meta`, `inbox`, and
`execution`. Privileged groups (`lifecycle`, `approvals`) `execution`. The privileged `lifecycle` group
are opt-in via the P3RM1SS10NS tab. is opt-in via the P3RM1SS10NS tab.
## Core tools (always available) ## Core tools (always available)
@ -61,14 +61,10 @@ are opt-in via the P3RM1SS10NS tab.
Transient pings (sentinel id 0) have nothing to ack and show no marker. Transient pings (sentinel id 0) have nothing to ack and show no marker.
**System messages** (from sender `system`): the broker delivers **System messages** (from sender `system`): the broker delivers
`hive_sh4re::manager::HelperEvent` (three variants: `ApprovalResolved`, `hive_sh4re::manager::HelperEvent` (two variants: `ContainerCrash`,
`ContainerCrash`, `NeedsUpdate` — declared, but no call site `NeedsUpdate` — declared, but no call site constructs it) as regular
constructs it) as regular inbox messages (same `recv` inbox messages (same `recv` path; body is a JSON object with an `event`
path; body is a JSON object with an `event` discriminant field). discriminant field).
`ApprovalResolved` goes to whichever agent actually submitted the
approval (`Coordinator::notify_submitter`, looked up from the
authenticated socket caller at submit time; a row with no
recorded submitter falls back to the manager, `ruth`).
`ContainerCrash` always goes to `ruth` (`Coordinator::notify_manager`, `ContainerCrash` always goes to `ruth` (`Coordinator::notify_manager`,
hardcoded — `hive-c0re/src/workers/crash_watch.rs`). Lifecycle hardcoded — `hive-c0re/src/workers/crash_watch.rs`). Lifecycle
transitions the job-queue scheduler or crash watcher drive directly — transitions the job-queue scheduler or crash watcher drive directly —
@ -84,15 +80,11 @@ at_unix_timestamp?)`.
- `get_loose_ends()` — list this agent's own open threads: pending - `get_loose_ends()` — list this agent's own open threads: pending
reminders, open todos pushed by an in-container subsystem (matrix, reminders, open todos pushed by an in-container subsystem (matrix,
bash, forge, or any user-configured MCP server), an undelivered-inbox bash, forge, or any user-configured MCP server), an undelivered-inbox
count, and unread matrix notifications; `approval` rows only appear count, and unread matrix notifications. Always the caller's own rows. Full per-variant wire shape in
when the caller is the manager (`ruth`) — sub-agents don't submit
approvals. Always the caller's own rows. Full per-variant wire shape in
[`docs/process/conventions.md` § Loose-ends wire [`docs/process/conventions.md` § Loose-ends wire
shape](../process/conventions.md#loose-ends-wire-shape). shape](../process/conventions.md#loose-ends-wire-shape).
- `cancel_loose_end` — hard-delete a `reminder`, cancel a pending - `cancel_loose_end` — hard-delete a `reminder` or clear a `todo` row
`approval` row, or clear a `todo` row (loose-ends-v2). Agents may (loose-ends-v2). Agents may only cancel rows they own.
only cancel rows they own; the `approval` kind is further restricted
to the root agent (`ruth`) server-side.
- `remind` — schedule a reminder in this agent's own inbox. Large - `remind` — schedule a reminder in this agent's own inbox. Large
payloads spill to `/agents/<self>/state/reminders/`. Pending count payloads spill to `/agents/<self>/state/reminders/`. Pending count
capped at 50 per agent (`HIVE_REMIND_MAX_PENDING_PER_AGENT`). capped at 50 per agent (`HIVE_REMIND_MAX_PENDING_PER_AGENT`).
@ -144,11 +136,9 @@ hive_name?, swarm_name?, matrix_accounts? }`. `matrix_accounts` is a
(claude or ACP, via `hive-runtime`) as background tasks, shipped (claude or ACP, via `hive-runtime`) as background tasks, shipped
default-on like bash execution; no tool group gates it. See default-on like bash execution; no tool group gates it. See
[`docs/tools/subagent.md`](../tools/subagent.md). [`docs/tools/subagent.md`](../tools/subagent.md).
- **Lifecycle + config** (`lifecycle`, `approvals`) — `approvals` - **Lifecycle + config** (`lifecycle`) — gates nothing.
gates `cancel_loose_end`'s approval-cancel arm server-side;
`lifecycle` gates nothing.
Config changes go through a forge PR on `agent-configs/<name>` — see Config changes go through a forge PR on `agent-configs/<name>` — see
[`docs/agent-lifecycle/approvals.md`](../agent-lifecycle/approvals.md). [`docs/agent-lifecycle/config-changes.md`](../agent-lifecycle/config-changes.md).
- **Forge repos** (`forge`) — gates nothing. - **Forge repos** (`forge`) — gates nothing.
- **Web egress** (`web_tools`) — enables Claude's built-in `WebFetch` - **Web egress** (`web_tools`) — enables Claude's built-in `WebFetch`
and `WebSearch` tools (not MCP tools; added directly to the and `WebSearch` tools (not MCP tools; added directly to the

View file

@ -1,6 +1,6 @@
# Dashboard & Web UI # Dashboard & Web UI
The per-hive dashboard: approvals, container state and the rebuild The per-hive dashboard: container state and the rebuild
queue for one hive. For swarm-wide day to day, see queue for one hive. For swarm-wide day to day, see
[the swarm UI](../swarm/ui.md) instead. If you want implementation [the swarm UI](../swarm/ui.md) instead. If you want implementation
detail — wire formats, DOM structure, event plumbing — see [Dashboard detail — wire formats, DOM structure, event plumbing — see [Dashboard
@ -20,8 +20,8 @@ of your time. It's a single page with exactly four tabs:
- **SW4RM** — every agent, live. This is the default tab and the one - **SW4RM** — every agent, live. This is the default tab and the one
you'll check most. you'll check most.
- **Y3R C4LL** — anything waiting on _you_: pending approvals. If an - **Y3R C4LL** — anything waiting on _you_: messages agents sent to
agent needs a decision from you, it's here. the operator.
- **P3RM1SS10NS** — what tools and system-level access each agent has. - **P3RM1SS10NS** — what tools and system-level access each agent has.
- **SCH3DUL3S** — a read-only list of the scheduled prompts left in the - **SCH3DUL3S** — a read-only list of the scheduled prompts left in the
hive, due for removal; operators set schedules at swarm level now. hive, due for removal; operators set schedules at swarm level now.
@ -48,14 +48,14 @@ whether it's running, what it's currently doing (a live status pill —
and quick links (stats, screen, forge profile). Select the name to open and quick links (stats, screen, forge profile). Select the name to open
its terminal and watch it work in real time. its terminal and watch it work in real time.
**Approve something an agent is waiting on.** Y3R C4LL is the one tab **Read what agents sent you.** Y3R C4LL is the one tab worth checking
worth checking regularly — it's everything that needs _you_. The tab's regularly — it's everything that needs _you_. The tab's count pill
count pill tells you at a glance if anything's pending. tells you at a glance if anything's unread.
**Review a config change.** Agent config changes (new packages, env **Review a config change.** Agent config changes (new packages, env
vars, MCP servers) are pull requests on the agent's `agent-configs` vars, MCP servers) are pull requests on the agent's `agent-configs`
repo. Review and merge them on the forge; the merge deploys the change. repo. Review and merge them on the forge; the merge deploys the change.
See [`approvals.md`](../agent-lifecycle/approvals.md#config-changes). See [`config-changes.md`](../agent-lifecycle/config-changes.md#config-changes).
**Start, stop, restart, or rebuild an agent.** Select one or more **Start, stop, restart, or rebuild an agent.** Select one or more
agents on SW4RM (select the icon) and use the selection bar, or use the agents on SW4RM (select the icon) and use the selection bar, or use the

View file

@ -24,7 +24,7 @@ from the dashboard tab strip.
standalone **C0R3** page (`/core.html`), and FL0W / L0GS / ST4TS standalone **C0R3** page (`/core.html`), and FL0W / L0GS / ST4TS
live on their own pages too, all reachable from the live on their own pages too, all reachable from the
H0M3 hub (not the tab strip). Count pills on SW4RM H0M3 hub (not the tab strip). Count pills on SW4RM
(container count), Y3R C4LL (pending approvals + unread (container count), Y3R C4LL (unread
operator messages), and SCH3DUL3S (active schedules left to copy). operator messages), and SCH3DUL3S (active schedules left to copy).
- **Banner-thin** (`░▒▓█▓▒░ HYPERHIVE / HIVE-C0RE / WE ARE THE WIRED ░▒▓█▓▒░`) - **Banner-thin** (`░▒▓█▓▒░ HYPERHIVE / HIVE-C0RE / WE ARE THE WIRED ░▒▓█▓▒░`)
— sits below the tab strip. — sits below the tab strip.
@ -46,13 +46,12 @@ from the dashboard tab strip.
snapshot. Hidden when there are no warnings. snapshot. Hidden when there are no warnings.
- **Browser tab title** — `hyperhive // h1ve-c0re` by default; updated - **Browser tab title** — `hyperhive // h1ve-c0re` by default; updated
to `<swarm> / <hive> // h1ve-c0re` once `hive_name` / `swarm_name` to `<swarm> / <hive> // h1ve-c0re` once `hive_name` / `swarm_name`
arrive in the state snapshot. When there are pending approvals, the arrive in the state snapshot. When there are unread operator messages,
page prepends a `(N)` prefix — `(3) pr1ma // h1ve-c0re` — so the the page prepends a `(N)` prefix — `(3) pr1ma // h1ve-c0re` — so the
operator can see the call count in an unfocused browser tab without operator can see the call count in an unfocused browser tab without
opening the dashboard. The page sets the prefix on the initial opening the dashboard. The page sets the prefix on the operator-inbox
`/api/state` cold-load and updates it live via cold-load and updates it live as broker `sent` events arrive; it's
`approval_resolved` SSE events; it's preserved when `hive_name` / preserved when `hive_name` / `swarm_name` later replace the raw title.
`swarm_name` later replace the raw title.
The FL0W and L0GS pages use a slim header (a `← home` back-link + the The FL0W and L0GS pages use a slim header (a `← home` back-link + the
page title) rather than the dashboard tab strip — they're standalone page title) rather than the dashboard tab strip — they're standalone
@ -74,12 +73,7 @@ agent is stale.
## Y3R C4LL tab ## Y3R C4LL tab
Things blocked on operator decision — the approval queue. Things blocked on operator decision — the operator inbox.
**P3NDING APPR0VALS** — the queue (see "Approval card" below). A
`pending · N` / `history · N` tab pair switches between the live
queue and the last 30 resolved approvals (see "Approval card" for
the history row shape).
**0PER4T0R 1NB0X** — messages agents have sent to `to="operator"` but **0PER4T0R 1NB0X** — messages agents have sent to `to="operator"` but
the operator hasn't read yet. Cold-loaded from `/api/operator-inbox` on the operator hasn't read yet. Cold-loaded from `/api/operator-inbox` on
@ -332,7 +326,7 @@ named buckets of MCP tools; each agent starts with a role default
(sub-agents: `messaging`, `meta`, `inbox`, `execution` — (sub-agents: `messaging`, `meta`, `inbox`, `execution` —
`ToolGroup::AGENT_DEFAULT`; hive-c0re seeds the root agent to `ToolGroup::AGENT_DEFAULT`; hive-c0re seeds the root agent to
`ToolGroup::MANAGER_DEFAULT` — `messaging`, `meta`, `inbox`, `ToolGroup::MANAGER_DEFAULT` — `messaging`, `meta`, `inbox`,
`lifecycle`, `approvals`, `execution`, `lifecycle`, `execution`,
that is, every group except `forge` and `web_tools`). Checking / that is, every group except `forge` and `web_tools`). Checking /
unchecking stages which groups are active for the agent; the unchecking stages which groups are active for the agent; the
page-level **save all** button (below) commits it. Columns come from page-level **save all** button (below) commits it. Columns come from
@ -341,8 +335,7 @@ takes effect.
<!-- vale write-good.Passive = YES --> <!-- vale write-good.Passive = YES -->
The current tool groups are: `messaging`, `meta`, `inbox`, `lifecycle`, The current tool groups are: `messaging`, `meta`, `inbox`, `lifecycle`,
`approvals`, `forge`, `execution`, `forge`, `execution`, `web_tools`. All listed in `ToolGroup::ALL` in `hive-sh4re`. The
`web_tools`. All listed in `ToolGroup::ALL` in `hive-sh4re`. The
`web_tools` group is special: it carries no MCP tools; instead it adds `web_tools` group is special: it carries no MCP tools; instead it adds
Claude's built-in `WebFetch` and `WebSearch` to `--tools` / Claude's built-in `WebFetch` and `WebSearch` to `--tools` /
`--allowedTools` for that agent session. `--allowedTools` for that agent session.
@ -739,19 +732,17 @@ that wants step-level counts instead, unused here.
### Themed dialogs ### Themed dialogs
All confirmations, prompts, and transient error notices use an All confirmations and transient error notices use an in-app themed
in-app themed dialog system (`@hive/shared/modal.js`) rather than the dialog system (`@hive/shared/modal.js`) rather than the browser's
browser's native `confirm()` / `prompt()` / `alert()` chrome, so native `confirm()` / `alert()` chrome, so they match the Catppuccin
they match the Catppuccin palette and the OS can't style them away. palette and the OS can't style them away. Two primitives, both built
Three primitives, all built on the `openDialog` core: on the `openDialog` core:
- `themedConfirm({ message, danger, confirmLabel, checkboxes })` - `themedConfirm({ message, danger, confirmLabel, checkboxes })`
— a modal confirm that resolves to `null` on cancel or an object — a modal confirm that resolves to `null` on cancel or an object
of checkbox states on confirm. Destructive actions pass of checkbox states on confirm. Destructive actions pass
`danger: true` (the confirm button turns red and the cancel `danger: true` (the confirm button turns red and the cancel
button takes focus). Selecting the backdrop and `Esc` both cancel. button takes focus). Selecting the backdrop and `Esc` both cancel.
- `themedPrompt(...)` — a modal with a text input, resolving to the
entered string or `null`.
- `themedToast(message, { type, ... })` — a non-blocking toast (top-right, - `themedToast(message, { type, ... })` — a non-blocking toast (top-right,
`info` / `error` / `ok`) for transient validation + action `info` / `error` / `ok`) for transient validation + action
failures, so an error doesn't trap the operator behind a modal. failures, so an error doesn't trap the operator behind a modal.
@ -816,51 +807,18 @@ frosted-mauve bar slides up from the bottom of the viewport
Every render prunes stale selections (agents destroyed while Every render prunes stale selections (agents destroyed while
selected) before the bar appears. selected) before the bar appears.
## Approval card
Each pending approval renders as a card (`assets/call.js::
renderApprovals`) with three stacked sections:
- **identity header** — glyph, `#id`, agent, kind chip, and a
right-aligned `requested <N> ago` relative time from
`ApprovalView.requested_at`. Glyph and chip vary by kind:
| kind | glyph | chip |
|---|---|---|
| `update_meta_inputs` | `↻` | `meta-update` |
<!-- vale write-good.Passive = NO -->
The chip ticks live every second via a `data-requested-at`
attribute + client-side interval (no re-render). Turns amber once
the request has been pending ≥ 1h so a stale approval stands out;
the `.stale` class flips precisely at the 3600s boundary rather
than at the next `renderApprovals` call.
- **what-changed body** — the submitting agent's description, then the
kind's payload: the inputs to bump (`update_meta_inputs`).
- **decision actions** — `◆ APPR0VE` and `DENY`. Deny pops a
`prompt()` for an optional reason carried to the submitting agent as
`HelperEvent::ApprovalResolved.note`.
<!-- vale write-good.Passive = YES -->
A `pending · N` / `history · N` tab pair switches the section
between the live queue and the last 30 resolved approvals.
## Browser notifications ## Browser notifications
Pure frontend (`Notification` API). Two signals trigger them: Pure frontend (`Notification` API), triggered by a new broker message
sent `to: "operator"` (live via SSE).
- new pending approval (per id, delta on `/api/state`)
- new broker message sent `to: "operator"` (live via SSE)
The toggle controls live in the Y3R C4LL tab's ◆ PR3F3R3NC3S ◆ The toggle controls live in the Y3R C4LL tab's ◆ PR3F3R3NC3S ◆
section; see that section above for the user-facing shape. Dispatch section; see that section above for the user-facing shape. Dispatch
logic lives in `common.js::NOTIF`. logic lives in `common.js::NOTIF`.
<!-- vale write-good.Passive = NO --> <!-- vale write-good.Passive = NO -->
First `/api/state` after page load seeds "seen" sets without Only messages that arrive while the page is open count. Per-event tags
firing — only items that arrive while the page is open count. (`hyperhive:msg:<at>:<rand>`) so distinct events stack in the OS
Per-event tags (`hyperhive:approval:<id>`,
`hyperhive:msg:<at>:<rand>`) so distinct events stack in the OS
notification center instead of overwriting each other. notification center instead of overwriting each other.
`console.debug` logs at every block point (unsupported, `console.debug` logs at every block point (unsupported,
permission ungranted, muted) for in-browser debugging. Selecting it permission ungranted, muted) for in-browser debugging. Selecting it
@ -880,17 +838,6 @@ the H0M3 hub's **API** tile), with the raw spec at `/api/openapi.json`.
It's a growing supplement, not yet a full replacement for the list It's a growing supplement, not yet a full replacement for the list
below — some endpoints aren't in it yet. below — some endpoints aren't in it yet.
- `POST /api/approve/{id}` — approve a pending approval. Fires
`ApprovalResolved` on the dashboard event channel; client
updates derived approvals state from the event.
- `POST /api/deny/{id}` (`note=<reason>`, optional) — deny a pending
approval with an optional operator-supplied reason. The reason
travels to the submitting agent as `HelperEvent::ApprovalResolved.note`
and also rides on the dashboard's `ApprovalResolved` event.
Dashboard prompts via the themed `themedPrompt()` dialog on
selection — a resizable `<textarea>` where Enter submits and
Shift+Enter inserts a newline (so multi-sentence rejection
notes are possible).
- `POST /api/{rebuild,kill,restart,start,destroy}/{name}` — lifecycle. - `POST /api/{rebuild,kill,restart,start,destroy}/{name}` — lifecycle.
`destroy` accepts `purge=on` to also wipe state dirs. `destroy` accepts `purge=on` to also wipe state dirs.
- `POST /api/purge-tombstone/{name}` — wipe a tombstone's state dirs. - `POST /api/purge-tombstone/{name}` — wipe a tombstone's state dirs.
@ -1046,10 +993,6 @@ payload):
rows with a `↳ reply` tag that scroll-highlights the parent rows with a `↳ reply` tag that scroll-highlights the parent
row on selection. Used by the message-flow terminal renderer and row on selection. Used by the message-flow terminal renderer and
the operator-inbox derived state. the operator-inbox derived state.
- `approval_resolved` (id, agent, approval_kind, status,
resolved_at, note, description) — moves a row from the pending
queue to history. Client mutates a derived store and
re-renders only the approvals section.
- `transient_set` (name, transient_kind, since_unix) / - `transient_set` (name, transient_kind, since_unix) /
`transient_cleared` (name) — lifecycle action spinners. The `transient_cleared` (name) — lifecycle action spinners. The
client ticks the elapsed-seconds badge off `since_unix` client ticks the elapsed-seconds badge off `since_unix`
@ -1104,15 +1047,13 @@ payload):
`/api/state` is **only fetched on cold-load and on the few `/api/state` is **only fetched on cold-load and on the few
forms that mutate non-event-derived state** (PURG3 + forms that mutate non-event-derived state** (PURG3 +
meta-update, since tombstones + meta_inputs aren't event- meta-update, since tombstones + meta_inputs aren't event-
shaped yet). Every other section — approvals, shaped yet). Every other section — transients,
transients, containers, operator inbox, message flow — containers, operator inbox, message flow —
derives from `/api/dashboard/stream` after the initial snapshot, derives from `/api/dashboard/stream` after the initial snapshot,
maintaining its own client-side store and applying events on maintaining its own client-side store and applying events on
top. top.
<!-- vale write-good.Passive = YES --> <!-- vale write-good.Passive = YES -->
Generalised form helpers: `form[data-confirm="…"]` pops Generalised form helper: `form[data-confirm="…"]` pops
`confirm()` before submit; `form[data-prompt="…"]` pops `confirm()` before submit.
`prompt()` and stashes the answer in a hidden input named by
`data-prompt-field` (default `note`).

View file

@ -50,8 +50,7 @@
(per-agent) → `text/event-stream` SSE for live updates. The (per-agent) → `text/event-stream` SSE for live updates. The
dashboard stream carries broker `Sent` / `Delivered` (mirrored dashboard stream carries broker `Sent` / `Delivered` (mirrored
by a forwarder task from the broker's intra-process channel) by a forwarder task from the broker's intra-process channel)
plus mutation events (`approval_resolved`, plus mutation events (`transient_set` / `transient_cleared`). Each frame carries a `seq`. The
`transient_set` / `transient_cleared`). Each frame carries a `seq`. The
matching backfill endpoint is `GET /api/dashboard/history` (last matching backfill endpoint is `GET /api/dashboard/history` (last
~200 broker messages wrapped in `{ seq, events }`) on the ~200 broker messages wrapped in `{ seq, events }`) on the
dashboard and `GET /events/history` (last 2000 `LiveEvent`s dashboard and `GET /events/history` (last 2000 `LiveEvent`s
@ -262,8 +261,8 @@ that needs it rather than ripped out along with their one caller.
swipes in from the right — a singleton `#side-panel` with a swipes in from the right — a singleton `#side-panel` with a
titled header, a close button, and a scrollable body. Closes on titled header, a close button, and a scrollable body. Closes on
the button, selecting the backdrop, or `Escape`. `Panel.open(title, the button, selecting the backdrop, or `Escape`. `Panel.open(title,
node)` swaps the body; the JS builders for file previews, node)` swaps the body; the JS builders for file previews and
approval diffs, and journald logs all render into it. **The journald logs all render into it. **The
drawer width is drag-to-resize**: a thin 6px hit-strip on drawer width is drag-to-resize**: a thin 6px hit-strip on
the left edge captures pointer events, resizes the drawer in the left edge captures pointer events, resizes the drawer in
real-time (pointer capture keeps dragging even if the cursor real-time (pointer capture keeps dragging even if the cursor