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

@ -26,8 +26,8 @@ declarations.
## Agent lifecycle
- **How do config changes flow from manager to operator to container?** →
[`agent-lifecycle/approvals.md`](agent-lifecycle/approvals.md) (approval kinds, approval
state machine, `flake.lock` validation).
[`agent-lifecycle/config-changes.md`](agent-lifecycle/config-changes.md)
(config PRs, the meta flake, helper events).
- **What state survives destroy / purge / restart?** →
[`agent-lifecycle/persistence.md`](agent-lifecycle/persistence.md).
- **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
can't do itself. Nothing submits a new approval today: the one kind
left, a meta-input bump, only reads back from existing rows. Config
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.
Config changes are pull requests on the agent's config repo, which an
operator merges on the forge; that merge deploys them. Helper events are
how agents stay informed about lifecycle changes that need a reaction.
## 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
`agent-configs/<agent>`. Review it on the forge like any other PR, and
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
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.
No MCP tool exists for config changes: opening the PR IS the request.
1. The submitting agent (the child's parent, holding the `approvals`
tool group) **clones** `agent-configs/<name>`, edits it there (any
1. The submitting agent **clones** `agent-configs/<name>`, edits it there (any
tracked path, but `agent.nix` is the contract entry point), commits
with its own git identity, and pushes a branch + opens a PR with
`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
deploys nothing.
## Approval queue
### Withdrawing a pending approval
The submitting agent can call `cancel_loose_end(kind: "approval", id)` to
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.
Creating a brand-new agent is 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
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)).
### Approval kinds (wire shapes)
`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 -->
## Agent lifecycle
### Reminder delivery: file-path semantics
@ -144,10 +96,10 @@ body strings before calling it.
### Destroy semantics
`HostRequest::Destroy { name, purge }` is the lifecycle tear-down,
not an approval. Stops + removes the nspawn container, drops the
systemd drop-in, fails any pending approvals. Persistent state
(proposed/applied repos, claude credentials, `/state/` notes) is
`HostRequest::Destroy { name, purge }` is the lifecycle tear-down.
Stops + removes the nspawn container and drops the systemd drop-in.
Persistent state (proposed/applied repos, claude credentials, `/state/`
notes) is
**kept by default** — recreating the agent with the same name
reuses prior config + login. With `purge = true` the agent's
`/var/lib/hyperhive/{agents,applied}/<name>/` trees are also
@ -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
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
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
capability sees no other agent's config at all.
An agent with the `approvals` tool group submits a change the same way
it makes any other change: **clone the target agent's config repo from the
An agent submits a change to another agent's config the same way it
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
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.
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/agents/ruth/config/`. The root agent can edit its own
`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.
Differences from sub-agents:
@ -415,8 +332,6 @@ Differences from sub-agents:
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 "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,
pure transport with no dedicated helpers — it uses the same
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
varies is which MCP tools it surfaces (gated by tool groups and
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
the system prompt template.
## Helper events to the submitting agent
## Helper events
`Coordinator::notify_submitter(approval_id, &HelperEvent)` routes the
event to the agent that originally submitted the approval (looked up from
the `submitter` column on the `approvals` table). The harness delivers it
as a regular `system` inbox message so it drives a normal claude turn.
`finish_approval` fires an `ApprovalResolved` HelperEvent this way for
**every** approval kind's terminal state. A
"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`):
`Coordinator::notify_agent(agent, &HelperEvent)` pushes the event into
the agent's inbox, and the harness delivers it as a regular `system`
inbox message so it drives a normal claude turn. A "FYI, check when
convenient" event doesn't need a message — hive-c0re publishes those as
swarm lifecycle notices instead (`swarm_notices::notify`). 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-
running container went away with no operator-initiated transient
state (Stopping / Restarting / Destroying / Rebuilding) AND nothing

View file

@ -20,8 +20,6 @@ keeps its state, purging it doesn't.**
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
undelivered or delivered-but-not-yet-acked is always kept, however
old.
@ -40,8 +38,8 @@ schemas, file layouts, and internal migration mechanics.
### `/var/lib/hyperhive/db/broker.sqlite` (host)
Six tables, all in one file — two queues, a small key/value table,
the read-only schedule header/targets split, and the per-agent
Five tables, all in one file — the message queue, a small key/value
table, the read-only schedule header/targets split, and the per-agent
power-intent registry:
- `messages` — every inter-agent / operator-bound message.
@ -56,8 +54,6 @@ power-intent registry:
per-agent store — see [`/harness/` contents
below](#state-dirs-per-agent) for where reminders (and todos)
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
moved to swarm level ([`swarm/schedules.md`](../swarm/schedules.md)).
`owner / body / interval_seconds (NULL = one-shot) /
@ -86,8 +82,6 @@ Retention:
rows are always kept — the harness `ack_turn`s only after a
successful turn, so `requeue_inflight` can still requeue
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.
- `agent_power` rows live until the operator destroys the agent (one row per
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
applied repo. Tracks `flake.nix` (module-only boilerplate; never
edited after first spawn) + `agent.nix` (the actual config; the
root agent's edits land here via the approval flow) + any other
files committed via the approval flow. `.git/` carries the proposal /
approved / building / deployed / failed / denied tag history.
edited after first spawn) + `agent.nix` (the actual config; edits land
here from merged config PRs) + any other files those PRs commit.
`.git/` carries the proposal / approved / building / deployed / failed /
denied tag history.
Under `/var/lib/hyperhive/meta/` — the swarm-wide deploy flake plus
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
an agent's state. The mechanics, for completeness:
- `DESTR0Y` also drops the systemd drop-in and fails any pending
approvals; reviving the agent is `swarmctl agent create` with the same
name and hive, which reuses the kept state.
- `DESTR0Y` also drops the systemd drop-in; reviving the agent is
`swarmctl agent create` with the same name and hive, which reuses the
kept state.
- `PURG3` wipes `/var/lib/hyperhive/{agents,applied}/<name>/` — the
union of everything `DESTR0Y` left behind.
`actions::destroy` implements the root/bootstrap agent's specialness as a soft policy
guard that refuses to destroy it, backstopped by
`auto_update::ensure_root_agent`, which recreates it on the next
hive-c0re startup if it's ever absent (bypassing the approval queue,
as required infrastructure) — so even without the guard, destroying it
would only be transient.
hive-c0re startup if it's ever absent — so even without the guard,
destroying it would only be transient.
### 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
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
@ -150,7 +150,6 @@ hivectl list-agents # this hive's agents, status, rev
hivectl agent <name> restart # stop + start, no rebuild
hivectl agent <name> watch # follow its live event stream
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)
```

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
`main` directly nor self-merge. An operator's merge in the Forgejo UI
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
add-only status tags and `main` without force, and treats a refused
`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`.)
hive-c0re also references this repo as the agent's **persistent meta
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 —
so the config lives on the forge, not a hand-synced local checkout.
- 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.
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

View file

@ -163,27 +163,18 @@ the dashboard's "mark all read" (unbounded, per-agent).
### Loose-ends wire shape
`LooseEnd` is the per-row response shape for `GetLooseEnds` — one
request shape, no per-socket flavours. Tagged enum so
new thread kinds (forge PRs, long-running approvals from a
privileged bot, etc.) can land later without breaking existing
handlers. Each row carries enough context that the caller renders
request shape, no per-socket flavours. Tagged enum so new thread kinds
(forge PRs, etc.) can land later without breaking existing handlers.
Each row carries enough context that the caller renders
it directly as a bulleted list, no follow-up fetch needed.
`GetLooseEnds` takes no target — it always returns the caller's own
rows, on both the agent and manager sockets alike (the manager, `ruth`,
is a normal agent here, just deployed automatically with different
default capabilities). `Approval` rows only appear when the calling agent is
the manager (sub-agents don't submit approvals). `Reminder` rows always
carry `owner == self`.
default capabilities). `Reminder` rows always carry `owner == self`.
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 }` —
`due_at` is the absolute time the scheduler is targeting (RFC
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
shows nonsense ages.
`CancelLooseEnd { kind, id }` is the matching write surface. The
`kind` enum (`Reminder` / `Approval`) selects which underlying store
the dispatcher reaches into. `Reminder` cancels from either surface
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.
`cancel_loose_end` is the matching write surface, handled in the
harness without a hive-c0re round-trip: `reminder` deletes one of the
caller's own pending reminders, `todo` clears one of its todos.
### Agent metadata
@ -296,7 +280,6 @@ binary flavor.
| `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`. |
| `lifecycle` | none |
| `approvals` | none — gates `cancel_loose_end`'s approval-cancel arm server-side. |
| `forge` | none |
| `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",
"inbox", "lifecycle"], "bob": ["messaging", "meta", "inbox"] }`. An absent entry
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
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
rejects multipart), calls `refreshState()` on success. New mutating
forms should add `data-async` and optionally `data-confirm` (for a
JS-side `confirm()` prompt) or `data-prompt="…"` (for a
`window.prompt()` whose answer goes into a hidden input named by
`data-prompt-field`, default `note`).
JS-side `confirm()` prompt).
`refreshState` defers automatically when `document.activeElement`
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`
brace and the tail `Reconcile` node. `flake.nix` itself lives in the agent's
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
re-applied here so a manual `↻ R3BU1LD` from the dashboard is
@ -412,7 +393,7 @@ sufficient to recover.
## 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
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`,
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
### 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
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 /
build-log. Deps are intra-DAG edges only (`AfterOk` by default:
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` |
| `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 |
| `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 |
| `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 |
| `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 |
@ -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)
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»)
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»
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;
@ -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
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
`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
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`,
`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`.
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
- `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/process/conventions.md` — wire protocol, recipient sentinels
- `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
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
[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
operator recovers by redelivering that delivery from the forge hook page,
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
per-agent page) through the gateway, on one origin.
The core daemon serves operator-authority actions (approve / deny,
answer-as-operator, lifecycle POSTs), reachable only via the
The core daemon serves operator-authority actions
(answer-as-operator, lifecycle POSTs), reachable only via the
gateway.
- **Agent** — speaks only for itself, only over its per-agent
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
etc.), though — there, operator-merge is process + accepted risk, not a
technical control.
- **Approvals** — config changes are config PRs an operator merges on the
forge (see [`approvals.md`](../agent-lifecycle/approvals.md)), and
- **Operator sign-off** — config changes are config PRs an operator merges
on the forge (see
[`config-changes.md`](../agent-lifecycle/config-changes.md)), and
scheduled prompts are operator-only, set at swarm level
(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 |
| ---------------------------- | -------------- | ------------------------------------------------------------ |
| `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-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
preset (`AGENT_DEFAULT`) includes `messaging`, `meta`, `inbox`, and
`execution`. Privileged groups (`lifecycle`, `approvals`)
are opt-in via the P3RM1SS10NS tab.
`execution`. The privileged `lifecycle` group
is opt-in via the P3RM1SS10NS tab.
## 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.
**System messages** (from sender `system`): the broker delivers
`hive_sh4re::manager::HelperEvent` (three variants: `ApprovalResolved`,
`ContainerCrash`, `NeedsUpdate` — declared, but no call site
constructs it) as regular inbox messages (same `recv`
path; body is a JSON object with an `event` 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`).
`hive_sh4re::manager::HelperEvent` (two variants: `ContainerCrash`,
`NeedsUpdate` — declared, but no call site constructs it) as regular
inbox messages (same `recv` path; body is a JSON object with an `event`
discriminant field).
`ContainerCrash` always goes to `ruth` (`Coordinator::notify_manager`,
hardcoded — `hive-c0re/src/workers/crash_watch.rs`). Lifecycle
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
reminders, open todos pushed by an in-container subsystem (matrix,
bash, forge, or any user-configured MCP server), an undelivered-inbox
count, and unread matrix notifications; `approval` rows only appear
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
count, and unread matrix notifications. Always the caller's own rows. Full per-variant wire shape in
[`docs/process/conventions.md` § Loose-ends wire
shape](../process/conventions.md#loose-ends-wire-shape).
- `cancel_loose_end` — hard-delete a `reminder`, cancel a pending
`approval` row, or clear a `todo` row (loose-ends-v2). Agents may
only cancel rows they own; the `approval` kind is further restricted
to the root agent (`ruth`) server-side.
- `cancel_loose_end` — hard-delete a `reminder` or clear a `todo` row
(loose-ends-v2). Agents may only cancel rows they own.
- `remind` — schedule a reminder in this agent's own inbox. Large
payloads spill to `/agents/<self>/state/reminders/`. Pending count
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
default-on like bash execution; no tool group gates it. See
[`docs/tools/subagent.md`](../tools/subagent.md).
- **Lifecycle + config** (`lifecycle`, `approvals`) — `approvals`
gates `cancel_loose_end`'s approval-cancel arm server-side;
`lifecycle` gates nothing.
- **Lifecycle + config** (`lifecycle`) — gates nothing.
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.
- **Web egress** (`web_tools`) — enables Claude's built-in `WebFetch`
and `WebSearch` tools (not MCP tools; added directly to the

View file

@ -1,6 +1,6 @@
# 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
[the swarm UI](../swarm/ui.md) instead. If you want implementation
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
you'll check most.
- **Y3R C4LL** — anything waiting on _you_: pending approvals. If an
agent needs a decision from you, it's here.
- **Y3R C4LL** — anything waiting on _you_: messages agents sent to
the operator.
- **P3RM1SS10NS** — what tools and system-level access each agent has.
- **SCH3DUL3S** — a read-only list of the scheduled prompts left in the
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
its terminal and watch it work in real time.
**Approve something an agent is waiting on.** Y3R C4LL is the one tab
worth checking regularly — it's everything that needs _you_. The tab's
count pill tells you at a glance if anything's pending.
**Read what agents sent you.** Y3R C4LL is the one tab worth checking
regularly — it's everything that needs _you_. The tab's count pill
tells you at a glance if anything's unread.
**Review a config change.** Agent config changes (new packages, env
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.
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
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
live on their own pages too, all reachable from the
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).
- **Banner-thin** (`░▒▓█▓▒░ HYPERHIVE / HIVE-C0RE / WE ARE THE WIRED ░▒▓█▓▒░`)
— sits below the tab strip.
@ -46,13 +46,12 @@ from the dashboard tab strip.
snapshot. Hidden when there are no warnings.
- **Browser tab title** — `hyperhive // h1ve-c0re` by default; updated
to `<swarm> / <hive> // h1ve-c0re` once `hive_name` / `swarm_name`
arrive in the state snapshot. When there are pending approvals, the
page prepends a `(N)` prefix — `(3) pr1ma // h1ve-c0re` — so the
arrive in the state snapshot. When there are unread operator messages,
the page prepends a `(N)` prefix — `(3) pr1ma // h1ve-c0re` — so the
operator can see the call count in an unfocused browser tab without
opening the dashboard. The page sets the prefix on the initial
`/api/state` cold-load and updates it live via
`approval_resolved` SSE events; it's preserved when `hive_name` /
`swarm_name` later replace the raw title.
opening the dashboard. The page sets the prefix on the operator-inbox
cold-load and updates it live as broker `sent` events arrive; it's
preserved when `hive_name` / `swarm_name` later replace the raw title.
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
@ -74,12 +73,7 @@ agent is stale.
## Y3R C4LL tab
Things blocked on operator decision — the approval queue.
**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).
Things blocked on operator decision — the operator inbox.
**0PER4T0R 1NB0X** — messages agents have sent to `to="operator"` but
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` —
`ToolGroup::AGENT_DEFAULT`; hive-c0re seeds the root agent to
`ToolGroup::MANAGER_DEFAULT` — `messaging`, `meta`, `inbox`,
`lifecycle`, `approvals`, `execution`,
`lifecycle`, `execution`,
that is, every group except `forge` and `web_tools`). Checking /
unchecking stages which groups are active for the agent; the
page-level **save all** button (below) commits it. Columns come from
@ -341,8 +335,7 @@ takes effect.
<!-- vale write-good.Passive = YES -->
The current tool groups are: `messaging`, `meta`, `inbox`, `lifecycle`,
`approvals`, `forge`, `execution`,
`web_tools`. All listed in `ToolGroup::ALL` in `hive-sh4re`. The
`forge`, `execution`, `web_tools`. All listed in `ToolGroup::ALL` in `hive-sh4re`. The
`web_tools` group is special: it carries no MCP tools; instead it adds
Claude's built-in `WebFetch` and `WebSearch` to `--tools` /
`--allowedTools` for that agent session.
@ -739,19 +732,17 @@ that wants step-level counts instead, unused here.
### Themed dialogs
All confirmations, prompts, and transient error notices use an
in-app themed dialog system (`@hive/shared/modal.js`) rather than the
browser's native `confirm()` / `prompt()` / `alert()` chrome, so
they match the Catppuccin palette and the OS can't style them away.
Three primitives, all built on the `openDialog` core:
All confirmations and transient error notices use an in-app themed
dialog system (`@hive/shared/modal.js`) rather than the browser's
native `confirm()` / `alert()` chrome, so they match the Catppuccin
palette and the OS can't style them away. Two primitives, both built
on the `openDialog` core:
- `themedConfirm({ message, danger, confirmLabel, checkboxes })`
— a modal confirm that resolves to `null` on cancel or an object
of checkbox states on confirm. Destructive actions pass
`danger: true` (the confirm button turns red and the 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,
`info` / `error` / `ok`) for transient validation + action
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
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
Pure frontend (`Notification` API). Two signals trigger them:
- new pending approval (per id, delta on `/api/state`)
- new broker message sent `to: "operator"` (live via SSE)
Pure frontend (`Notification` API), triggered by a new broker message
sent `to: "operator"` (live via SSE).
The toggle controls live in the Y3R C4LL tab's ◆ PR3F3R3NC3S ◆
section; see that section above for the user-facing shape. Dispatch
logic lives in `common.js::NOTIF`.
<!-- vale write-good.Passive = NO -->
First `/api/state` after page load seeds "seen" sets without
firing — only items that arrive while the page is open count.
Per-event tags (`hyperhive:approval:<id>`,
`hyperhive:msg:<at>:<rand>`) so distinct events stack in the OS
Only messages that arrive while the page is open count. Per-event tags
(`hyperhive:msg:<at>:<rand>`) so distinct events stack in the OS
notification center instead of overwriting each other.
`console.debug` logs at every block point (unsupported,
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
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.
`destroy` accepts `purge=on` to also wipe 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
row on selection. Used by the message-flow terminal renderer and
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_cleared` (name) — lifecycle action spinners. The
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
forms that mutate non-event-derived state** (PURG3 +
meta-update, since tombstones + meta_inputs aren't event-
shaped yet). Every other section — approvals,
transients, containers, operator inbox, message flow —
shaped yet). Every other section — transients,
containers, operator inbox, message flow —
derives from `/api/dashboard/stream` after the initial snapshot,
maintaining its own client-side store and applying events on
top.
<!-- vale write-good.Passive = YES -->
Generalised form helpers: `form[data-confirm="…"]` pops
`confirm()` before submit; `form[data-prompt="…"]` pops
`prompt()` and stashes the answer in a hidden input named by
`data-prompt-field` (default `note`).
Generalised form helper: `form[data-confirm="…"]` pops
`confirm()` before submit.

View file

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