Watch
0
0
Fork
You've already forked hyperhive
0

docs: handbook states what the code does (fact sweep fixes)

This commit is contained in:
atlas 2026-11-10 00:51:59 +01:00 • committed by mara
commit 409e7ee5a0
7 changed files with 64 additions and 69 deletions

View file

@ -41,8 +41,8 @@ should cover._ `topology::all_agents` is the only reader that matters.
That reader is a permission boundary: the set it returns is what an agent That reader is a permission boundary: the set it returns is what an agent
holding the `ManageRootAgent` permission gets bind-mounted (each other holding the `ManageRootAgent` permission gets bind-mounted (each other
agent's `state` read-write and `config` read-only; never `harness`). An agent's `state` dir, read-write, at `/agents/<name>/state`; never `config` or
agent holding no permission sees its own dirs and nothing else: `harness`). An agent holding no permission sees its own dirs and nothing else:
`ManageRootAgent` is the only grant that reaches another agent's state `ManageRootAgent` is the only grant that reaches another agent's state
dir. See dir. See
[`persistence.md`](persistence.md)'s _Cross-agent access to state._ [`persistence.md`](persistence.md)'s _Cross-agent access to state._
@ -66,12 +66,14 @@ system-level facts live.
degrades safely to "no agents" (covers a fresh install that hasn't degrades safely to "no agents" (covers a fresh install that hasn't
synced yet). synced yet).
- **Reconcile** — runs alongside the periodic meta/flake regeneration. - **Reconcile** — runs alongside the periodic meta/flake regeneration.
Adds newly spawned agents, drops removed ones. Reconcile keeps agents Adds newly spawned agents, drops removed ones. The live set is the
whose config repo exists but that haven't spawned yet, so the gap until hive's existing agent containers plus the agent that spawns now; reconcile
the container appears doesn't churn the file. drops an agent whose config repo exists but whose container doesn't.
No write API and no operator verb reach this file. Reconcile derives it No write API and no operator verb reach this file. Reconcile derives it
from which agents exist, so the next pass overwrites a hand edit. from which agents exist, so the next pass overwrites a hand edit that
parses. It refuses to write over a file it can't parse and fails
instead, so a broken hand edit stays until someone fixes it.
See `hive-c0re/src/agent_config/topology.rs` and `hive-c0re/src/meta.rs`'s See `hive-c0re/src/agent_config/topology.rs` and `hive-c0re/src/meta.rs`'s
module docs for the exact call chain. module docs for the exact call chain.
@ -99,9 +101,9 @@ state dirs work as for every other agent.
[`docs/4-implementation/turn-loop/mcp.md`](../../4-implementation/turn-loop/mcp.md#waking-the-agent-from-inside-the-container)). [`docs/4-implementation/turn-loop/mcp.md`](../../4-implementation/turn-loop/mcp.md#waking-the-agent-from-inside-the-container)).
- **Storage/mounts** — the `manage_root_agent` permission, which the swarm - **Storage/mounts** — the `manage_root_agent` permission, which the swarm
UI's setup assistant creates ruth with, gets a container UI's setup assistant creates ruth with, gets a container
`/var/lib/hyperhive/agents` bind-mounted RW at `/agents` (so it can each other agent's `state` dir bind-mounted RW at
manage any agent's state dir; config isn't mounted, since a real `/agents/<name>/state` (so it can manage any agent's state dir; config
config change is a PR from a clone), plus an RO mount for isn't mounted, since a real config change is a PR from a clone), plus an RO mount for
`/meta` (the hive's topology and limits files). Any agent holding it gets the same mounts. `/meta` (the hive's topology and limits files). Any agent holding it gets the same mounts.
- **Prompt** — identical for every container but one block (below): - **Prompt** — identical for every container but one block (below):
`prompts/system.md` `prompts/system.md`

View file

@ -70,26 +70,21 @@ merged by an operator on the forge (see [Config changes](#config-changes)).
### Reminder delivery: file-path semantics ### Reminder delivery: file-path semantics
A reminder may carry a `file_path` (the agent-visible path inside its A reminder may carry a `file_path` (the agent-visible path inside its
container, for example `/agents/<name>/state/foo.md`). On delivery hive-c0re: container, for example `/agents/<name>/state/foo.md`). The agent's own
harness (`hive-agent`, inside the container) delivers the reminder when it
falls due:
1. **Translates** the container path to the host path 1. **Validates** the path: it must resolve under the agent's own state
(`/var/lib/hyperhive/agents/<name>/state/foo.md`) so c0re can write directory, with no `..` components and a non-empty relative tail. On
from outside the container. rejection, or on a write failure, the harness delivers the original
2. **Validates** the path: rejects anything outside the agent's own state message inline with a warning note — the reminder still fires.
subtree, containing `..` (path traversal), or with an empty relative 2. **Writes the body to disk** at that path and delivers a short pointer
tail. On rejection hive-c0re skips the write and delivers the message in its place, keeping the agent's inbox / wake-prompt small
original message inline with a warning — the reminder still fires. while the agent reads the bulky payload out of band.
3. **Defends against symlink escape**: after `create_dir_all`, hive-c0re
canonicalizes the parent dir and re-verifies it lives under the agent's host
state root. hive-c0re opens the final file with
`O_NOFOLLOW | O_CREAT | O_TRUNC` so an existing symlink at the
basename can't redirect the write to an arbitrary host path.
4. **Writes the body to disk** and delivers a short pointer message in its
place, keeping the agent's inbox / wake-prompt small while the agent
reads the bulky payload out of band.
`Broker::deliver_reminders_batch` handles atomicity of the inbox INSERT + `reminders.sent_at` UPDATE; the scheduler only computes the The write happens inside the agent's container, so hive-c0re takes no
body strings before calling it. part in it and there is no symlink-escape defence: the harness is the
agent, and the container boundary is the sandbox.
### Destroy semantics ### Destroy semantics
@ -122,8 +117,9 @@ deploys.
What `hive-c0re` still keeps under `/var/lib/hyperhive/meta/` is plain What `hive-c0re` still keeps under `/var/lib/hyperhive/meta/` is plain
state, bound read-only into the root agent's container as `/meta`: state, bound read-only into the root agent's container as `/meta`:
`topology.json`, the swarm resource limits, and the permission seed files of a `topology.json` and the swarm resource limits. The permissions the swarm
hive that predates the swarm's permissions. `agent_set::refresh(agents: grants live outside it, in `/var/lib/hyperhive/swarm-agent-permissions.json`.
`agent_set::refresh(agents:
&[AgentSpec])` keeps `topology.json`, the gateway's agent-socket map and its &[AgentSpec])` keeps `topology.json`, the gateway's agent-socket map and its
nginx include in line with the live agents. `provision_container`, `destroy`, nginx include in line with the live agents. `provision_container`, `destroy`,
`rebuild` and the startup migration call it. It refuses an empty agent list, `rebuild` and the startup migration call it. It refuses an empty agent list,
@ -243,8 +239,8 @@ mounts via `set_nspawn_flags`:
- `/var/lib/hyperhive/meta/` → `/meta/` (RO) — the hive's plain state - `/var/lib/hyperhive/meta/` → `/meta/` (RO) — the hive's plain state
(topology, swarm limits). (topology, swarm limits).
An agent without the permission only has its direct children's config An agent without the permission gets only its own `state`, `harness` and
dirs. `work` dirs: no other agent's dirs and no `/meta`.
⚠️ nspawn bakes bind flags at container start, so hive-c0re restarts ⚠️ nspawn bakes bind flags at container start, so hive-c0re restarts
an agent, after its current turn, when its `manage_root_agent` changes. an agent, after its current turn, when its `manage_root_agent` changes.

View file

@ -9,8 +9,8 @@ keeps its state, purging it doesn't.**
- **`DESTR0Y`** (the default action) stops and removes the container - **`DESTR0Y`** (the default action) stops and removes the container
but keeps everything on disk — config history, claude login, `/state/` but keeps everything on disk — config history, claude login, `/state/`
notes, harness data. The agent shows up as a tombstone (K3PT ST4T3 on notes, harness data. The agent shows up as a tombstone (the K3PT ST4T3
the C0R3 page); `swarmctl agent create` with the same name and hive tab of the dashboard); `swarmctl agent create` with the same name and hive
recreates it from the kept state, **no re-login needed**. recreates it from the kept state, **no re-login needed**.
- **`PURG3`** (opt-in, from the dashboard or `hivectl agent <name> - **`PURG3`** (opt-in, from the dashboard or `hivectl agent <name>
destroy --purge`) is `DESTR0Y` plus wiping all of it — config destroy --purge`) is `DESTR0Y` plus wiping all of it — config
@ -235,8 +235,8 @@ Unusually, it's read and written from **both** sides of the harness
bind-mount, and that's the whole design: the harness stats it bind-mount, and that's the whole design: the harness stats it
in-container via `hive-agent`'s `paths::paused_marker`, while hive-c0re in-container via `hive-agent`'s `paths::paused_marker`, while hive-c0re
stats it on the host (`Coordinator::is_paused`) to populate the stats it on the host (`Coordinator::is_paused`) to populate the
`paused` field on the agent card, and creates/removes it `paused` field on the agent card, and asks hive-priv to create or remove it
(`Coordinator::set_paused`) for `hivectl agent <name> pause|resume` and the (`Coordinator::set_paused`; hive-c0re itself is unprivileged) for `hivectl agent <name> pause|resume` and the
dashboard toggle. Because the file itself is the only shared state dashboard toggle. Because the file itself is the only shared state
there's no protocol between them, no round-trip into the container, and there's no protocol between them, no round-trip into the container, and
pause keeps working when the harness is wedged or the container is pause keeps working when the harness is wedged or the container is

View file

@ -44,10 +44,16 @@ at a given viewport width. Two columns:
- **Pills cluster** (`.agent-header-pills`, right-aligned): status - **Pills cluster** (`.agent-header-pills`, right-aligned): status
badges (`StatusChips.tsx`) and flyout triggers together in one row, badges (`StatusChips.tsx`) and flyout triggers together in one row,
so the header reads as one identity zone + one status/actions zone: so the header reads as one identity zone + one status/actions zone:
- **Alive badge**: `● alive` (green) / `⊘ rate limited` (red) / - **Status badge**, one badge for liveness and turn state: `● alive`
`◌ needs login` / `◌ logging in` / `○ offline` / `… connecting`. (green) / `⊘ rate limited` (red) / `◌ needs login` / `◌ logging in` /
- **State badge**: `💤 idle` / `🧠 thinking` / `📦 compacting` / `○ offline` / `… connecting` / `💤 idle` / `🧠 thinking` /
`○ offline` / `… booting` + age suffix. `📦 compacting` / `… booting` + age suffix, or paused. Selecting it
opens a menu: `⏸ pause` / `▶ resume` (via the same hive-c0re-owned
`/api/pause/<name>` / `/api/resume/<name>` endpoints the dashboard
uses; pausing needs a write this unprivileged process can't make
directly), `■ cancel turn` (only while a turn is running),
`↻ new session` and `🔓 logout`. Cancel turn, new session and logout
need a second selection to confirm.
- **Model and effort badges** (`model · <name>`, `effort · <level>`): - **Model and effort badges** (`model · <name>`, `effort · <level>`):
display only. Each tooltip says where the value comes from display only. Each tooltip says where the value comes from
(`state.model_source` / `state.effort_source`: set on the swarm, (`state.model_source` / `state.effort_source`: set on the swarm,
@ -65,10 +71,6 @@ at a given viewport width. Two columns:
- **Ctx / cost badges**: `ctx · 142k` (last inference's prompt size, - **Ctx / cost badges**: `ctx · 142k` (last inference's prompt size,
tooltip shows % of context window) / `cost · 1.3M` (cumulative tooltip shows % of context window) / `cost · 1.3M` (cumulative
tokens billed across every inference in the last turn). tokens billed across every inference in the last turn).
- **Pause badge** (`⏸ pause` / `▶ resume`): toggles via the same
hive-c0re-owned `/api/pause/<name>` / `/api/resume/<name>`
endpoints the dashboard uses (pausing needs a write this
unprivileged process can't make directly).
- **Inbox / todos pills** (`📬 inbox · N` / `📋 todos · N`): hidden - **Inbox / todos pills** (`📬 inbox · N` / `📋 todos · N`): hidden
when empty; selecting it opens the matching flyout in the side panel. when empty; selecting it opens the matching flyout in the side panel.
- **Profile avatar**: the signed-in user's initial, opening a popover with - **Profile avatar**: the signed-in user's initial, opening a popover with
@ -162,7 +164,7 @@ If the todo store errors or doesn't answer for any id, the endpoint returns
## Live view ## Live view
Each agent runs an `events::Bus`: a `tokio::sync::broadcast<LiveEvent>` Each agent runs an `events::Bus`: a `tokio::sync::broadcast<LiveEvent>`
plus a sqlite-backed history at `/state/hyperhive-events.sqlite`. plus a sqlite-backed history at `/harness/hyperhive-events.sqlite`.
The harness emits `TurnStart { from, body, unread }`, The harness emits `TurnStart { from, body, unread }`,
`Stream(value)` (one per parsed stream-json line), `Note`, `Stream(value)` (one per parsed stream-json line), `Note`,
`TurnEnd { ok, note }`. Each event also carries a `ts` (unix `TurnEnd { ok, note }`. Each event also carries a `ts` (unix
@ -238,10 +240,9 @@ on this page calls it now.
Successful POSTs return 200 (no 303 redirects). Error responses Successful POSTs return 200 (no 303 redirects). Error responses
use semantic status codes: **400** for missing/malformed input use semantic status codes: **400** for missing/malformed input
(`body` required, unknown model name, unrecognised effort level), (`/api/todos/mark-done` with no todo ids), **500** only for genuine
**409** for retryable state conflicts (turn in flight when server/transport failures. `/compact` never conflicts: it queues
`/compact` fires), **500** only for genuine server/transport and runs at the end of the current turn. The matching
failures. The matching
mutations fire `LiveEvent` variants on the per-agent bus, so the mutations fire `LiveEvent` variants on the per-agent bus, so the
client doesn't refetch `/api/state` on submit — the SSE stream client doesn't refetch `/api/state` on submit — the SSE stream
delivers the new state faster anyway. delivers the new state faster anyway.

View file

@ -10,7 +10,7 @@ matrix accounts, the controller's job graph — are on the
hive-c0re serves the dashboard at `/`. hive-c0re serves the dashboard at `/`.
It has a fixed chrome header at the top and a `<main>` that shows exactly It has a fixed chrome header at the top and a `<main>` that shows exactly
one tab pane at a time. The URL hash (`#agents`, `#rebuild`, `#meta`, one tab pane at a time. The URL hash (`#agents`, `#rebuild`,
`#kept`) drives which pane is `#kept`) drives which pane is
active; hash changes don't reload the page. active; hash changes don't reload the page.
@ -428,7 +428,8 @@ the pages menu's **api docs** entry), 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/{rebuild,kill,restart,start,destroy}/{name}` — lifecycle. - `POST /api/{rebuild,kill,restart,start,destroy,pause,resume}/{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.
- `POST /api/rebuild-queue/{id}/cancel` — drop a `Queued` entry. - `POST /api/rebuild-queue/{id}/cancel` — drop a `Queued` entry.
@ -441,6 +442,10 @@ below — some endpoints aren't in it yet.
not yet delivered and sets `acked_at = now`. Returns not yet delivered and sets `acked_at = now`. Returns
`{ "marked": N }`. Agent name validated against `{ "marked": N }`. Agent name validated against
`[a-z0-9_-]`, 1-63 chars; 400 on bad input. `[a-z0-9_-]`, 1-63 chars; 400 on bad input.
- `GET /api/build-logs?limit=N` — most-recent build log headers across
all agents (`limit` defaults to 30).
- `GET /api/build-log/{node_id}` and `/api/build-log/{node_id}/raw` — the
build log of one job-queue node, as JSON and as `text/plain`.
- `GET /api/build-logs/{agent}?limit=N` — most-recent build log - `GET /api/build-logs/{agent}?limit=N` — most-recent build log
headers for one agent, newest first. Returns headers for one agent, newest first. Returns
`Vec<BuildLogHeader>` (JSON): `id`, `agent`, `kind`, `cmdline`, `Vec<BuildLogHeader>` (JSON): `id`, `agent`, `kind`, `cmdline`,
@ -458,11 +463,6 @@ below — some endpoints aren't in it yet.
- `GET /api/build-logs/id/{id}/raw` — raw concatenated - `GET /api/build-logs/id/{id}/raw` — raw concatenated
stdout+stderr as `text/plain`. Works on both finished and in-progress stdout+stderr as `text/plain`. Works on both finished and in-progress
build logs. build logs.
- `GET /api/reminders` — list pending reminders for the
dashboard's queued-reminders panel.
- `POST /api/cancel-reminder/{id}` — hard-delete a pending reminder.
- `POST /api/retry-reminder/{id}` — re-arm a reminder whose delivery
failed (clears the failure state so the scheduler retries).
- `GET /api/dashboard/stream` — unified live event channel: - `GET /api/dashboard/stream` — unified live event channel:
broker `sent` / `delivered`, plus the mutation events listed broker `sent` / `delivered`, plus the mutation events listed
@ -498,12 +498,6 @@ payload):
terminal-history trim). Unlike the snapshot events below it ships terminal-history trim). Unlike the snapshot events below it ships
no state at all: the client re-fetches `GET /api/jobq/graph`, which no state at all: the client re-fetches `GET /api/jobq/graph`, which
is where it reads the queue from cold too. is where it reads the queue from cold too.
- `reminders_changed` (seq, reminders: `Vec<PendingReminder>`) —
full snapshot of all pending reminders. Emitted after every
reminder mutation: agent `remind` calls (`agent_server`),
operator cancel / retry (`/api/system/reminders/*`), `cancel_loose_end`
with Reminder kind, and the scheduler tick after each delivery
batch (`reminder_scheduler`).
- `tombstones_changed` (seq, tombstones: `Vec<TombstoneView>`) — - `tombstones_changed` (seq, tombstones: `Vec<TombstoneView>`) —
full snapshot of destroyed-but-state-kept agents. Emitted on full snapshot of destroyed-but-state-kept agents. Emitted on
destroy, purge, and revive. The K3PT ST4T3 tab subscribes. destroy, purge, and revive. The K3PT ST4T3 tab subscribes.

View file

@ -63,9 +63,10 @@ resume drains the backlog rather than dropping it. Points worth knowing:
- **Sticky.** The marker lives on the persistent harness mount, so a - **Sticky.** The marker lives on the persistent harness mount, so a
paused agent stays paused across a container restart — and pausing a paused agent stays paused across a container restart — and pausing a
_stopped_ agent makes it come up parked. _stopped_ agent makes it come up parked.
- **Not a DAG.** Unlike `restart`/`stop`, there's no container operation - **A DAG, with an acknowledgement wait.** `pause` creates a DAG whose drain
to sequence, so it applies immediately with nothing to wait on. node waits for the harness to acknowledge the marker (giving up after
- **Stopping a paused agent is still fast.** hivectl skips the graceful-stop 3 minutes); `--no-wait` returns once the DAG exists.
- **Stopping a paused agent is still fast.** hive-c0re skips the graceful-stop
handshake for a paused agent (it would never answer), which handshake for a paused agent (it would never answer), which
is safe precisely because the pause check sits at the top of the loop: is safe precisely because the pause check sits at the top of the loop:
a paused agent has no turn in flight to checkpoint. a paused agent has no turn in flight to checkpoint.

View file

@ -484,9 +484,10 @@ would take delivery away from the first rather than add a recipient.
<!-- vale write-good.Passive = NO --> <!-- vale write-good.Passive = NO -->
**The `agent-configs` org is on it too.** The controller's hook is the **The `agent-configs` org is on it too.** The controller's hook is the
only one that acts on config PRs. Each hive deletes its own only one that acts on config PRs. No hive serves `/webhook/config-pr`
`/webhook/config-pr` org hook on startup, since a route no hive serves any more and none deletes it: a hook of that name left on the org by an
would otherwise sit on the org collecting failed deliveries. Removing older release stays registered and collects failed deliveries until an
operator deletes it by hand. Removing
the controller's hook stops forge-UI merges from deploying until its the controller's hook stops forge-UI merges from deploying until its
next start recreates it. next start recreates it.