From 409e7ee5a0022e1505dcd7c3c64078038e57fbb7 Mon Sep 17 00:00:00 2001 From: atlas Date: Tue, 10 Nov 2026 00:51:59 +0100 Subject: [PATCH] docs: handbook states what the code does (fact sweep fixes) --- docs/2-handbook/agents/agent-roster.md | 20 ++++++------ docs/2-handbook/agents/config-changes.md | 40 +++++++++++------------- docs/2-handbook/agents/persistence.md | 8 ++--- docs/2-handbook/agents/web-page.md | 27 ++++++++-------- docs/2-handbook/hives/dashboard.md | 20 +++++------- docs/2-handbook/hives/hivectl.md | 7 +++-- docs/2-handbook/swarm/README.md | 7 +++-- 7 files changed, 62 insertions(+), 67 deletions(-) diff --git a/docs/2-handbook/agents/agent-roster.md b/docs/2-handbook/agents/agent-roster.md index 2da2cd017..aa4765734 100644 --- a/docs/2-handbook/agents/agent-roster.md +++ b/docs/2-handbook/agents/agent-roster.md @@ -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 holding the `ManageRootAgent` permission gets bind-mounted (each other -agent's `state` read-write and `config` read-only; never `harness`). An -agent holding no permission sees its own dirs and nothing else: +agent's `state` dir, read-write, at `/agents//state`; never `config` or +`harness`). An agent holding no permission sees its own dirs and nothing else: `ManageRootAgent` is the only grant that reaches another agent's state dir. See [`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 synced yet). - **Reconcile** — runs alongside the periodic meta/flake regeneration. - Adds newly spawned agents, drops removed ones. Reconcile keeps agents - whose config repo exists but that haven't spawned yet, so the gap until - the container appears doesn't churn the file. + Adds newly spawned agents, drops removed ones. The live set is the + hive's existing agent containers plus the agent that spawns now; reconcile + 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 -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 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)). - **Storage/mounts** — the `manage_root_agent` permission, which the swarm UI's setup assistant creates ruth with, gets a container - `/var/lib/hyperhive/agents` bind-mounted RW at `/agents` (so it can - manage any agent's state dir; config isn't mounted, since a real - config change is a PR from a clone), plus an RO mount for + each other agent's `state` dir bind-mounted RW at + `/agents//state` (so it can manage any agent's state dir; config + 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. - **Prompt** — identical for every container but one block (below): `prompts/system.md` diff --git a/docs/2-handbook/agents/config-changes.md b/docs/2-handbook/agents/config-changes.md index c73e2b8e5..44064848e 100644 --- a/docs/2-handbook/agents/config-changes.md +++ b/docs/2-handbook/agents/config-changes.md @@ -70,26 +70,21 @@ merged by an operator on the forge (see [Config changes](#config-changes)). ### Reminder delivery: file-path semantics A reminder may carry a `file_path` (the agent-visible path inside its -container, for example `/agents//state/foo.md`). On delivery hive-c0re: +container, for example `/agents//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 - (`/var/lib/hyperhive/agents//state/foo.md`) so c0re can write - from outside the container. -2. **Validates** the path: rejects anything outside the agent's own state - subtree, containing `..` (path traversal), or with an empty relative - tail. On rejection hive-c0re skips the write and delivers the - original message inline with a warning — the reminder still fires. -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. +1. **Validates** the path: it must resolve under the agent's own state + directory, with no `..` components and a non-empty relative tail. On + rejection, or on a write failure, the harness delivers the original + message inline with a warning note — the reminder still fires. +2. **Writes the body to disk** at that path 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 -body strings before calling it. +The write happens inside the agent's container, so hive-c0re takes no +part in it and there is no symlink-escape defence: the harness is the +agent, and the container boundary is the sandbox. ### Destroy semantics @@ -122,8 +117,9 @@ deploys. What `hive-c0re` still keeps under `/var/lib/hyperhive/meta/` is plain 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 -hive that predates the swarm's permissions. `agent_set::refresh(agents: +`topology.json` and the swarm resource limits. The permissions the swarm +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 nginx include in line with the live agents. `provision_container`, `destroy`, `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 (topology, swarm limits). -An agent without the permission only has its direct children's config -dirs. +An agent without the permission gets only its own `state`, `harness` and +`work` dirs: no other agent's dirs and no `/meta`. ⚠️ nspawn bakes bind flags at container start, so hive-c0re restarts an agent, after its current turn, when its `manage_root_agent` changes. diff --git a/docs/2-handbook/agents/persistence.md b/docs/2-handbook/agents/persistence.md index 46a9c9dbc..b38949d15 100644 --- a/docs/2-handbook/agents/persistence.md +++ b/docs/2-handbook/agents/persistence.md @@ -9,8 +9,8 @@ keeps its state, purging it doesn't.** - **`DESTR0Y`** (the default action) stops and removes the container but keeps everything on disk — config history, claude login, `/state/` - notes, harness data. The agent shows up as a tombstone (K3PT ST4T3 on - the C0R3 page); `swarmctl agent create` with the same name and hive + notes, harness data. The agent shows up as a tombstone (the K3PT ST4T3 + tab of the dashboard); `swarmctl agent create` with the same name and hive recreates it from the kept state, **no re-login needed**. - **`PURG3`** (opt-in, from the dashboard or `hivectl agent 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 in-container via `hive-agent`'s `paths::paused_marker`, while hive-c0re stats it on the host (`Coordinator::is_paused`) to populate the -`paused` field on the agent card, and creates/removes it -(`Coordinator::set_paused`) for `hivectl agent pause|resume` and the +`paused` field on the agent card, and asks hive-priv to create or remove it +(`Coordinator::set_paused`; hive-c0re itself is unprivileged) for `hivectl agent pause|resume` and the dashboard toggle. Because the file itself is the only shared state 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 diff --git a/docs/2-handbook/agents/web-page.md b/docs/2-handbook/agents/web-page.md index 56d20921f..af5fd83d2 100644 --- a/docs/2-handbook/agents/web-page.md +++ b/docs/2-handbook/agents/web-page.md @@ -44,10 +44,16 @@ at a given viewport width. Two columns: - **Pills cluster** (`.agent-header-pills`, right-aligned): status badges (`StatusChips.tsx`) and flyout triggers together in one row, so the header reads as one identity zone + one status/actions zone: - - **Alive badge**: `● alive` (green) / `⊘ rate limited` (red) / - `◌ needs login` / `◌ logging in` / `○ offline` / `… connecting`. - - **State badge**: `💤 idle` / `🧠 thinking` / `📦 compacting` / - `○ offline` / `… booting` + age suffix. + - **Status badge**, one badge for liveness and turn state: `● alive` + (green) / `⊘ rate limited` (red) / `◌ needs login` / `◌ logging in` / + `○ offline` / `… connecting` / `💤 idle` / `🧠 thinking` / + `📦 compacting` / `… booting` + age suffix, or paused. Selecting it + opens a menu: `⏸ pause` / `▶ resume` (via the same hive-c0re-owned + `/api/pause/` / `/api/resume/` 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 · `, `effort · `): display only. Each tooltip says where the value comes from (`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, tooltip shows % of context window) / `cost · 1.3M` (cumulative tokens billed across every inference in the last turn). - - **Pause badge** (`⏸ pause` / `▶ resume`): toggles via the same - hive-c0re-owned `/api/pause/` / `/api/resume/` - endpoints the dashboard uses (pausing needs a write this - unprivileged process can't make directly). - **Inbox / todos pills** (`📬 inbox · N` / `📋 todos · N`): hidden when empty; selecting it opens the matching flyout in the side panel. - **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 Each agent runs an `events::Bus`: a `tokio::sync::broadcast` -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 }`, `Stream(value)` (one per parsed stream-json line), `Note`, `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 use semantic status codes: **400** for missing/malformed input -(`body` required, unknown model name, unrecognised effort level), -**409** for retryable state conflicts (turn in flight when -`/compact` fires), **500** only for genuine server/transport -failures. The matching +(`/api/todos/mark-done` with no todo ids), **500** only for genuine +server/transport failures. `/compact` never conflicts: it queues +and runs at the end of the current turn. The matching mutations fire `LiveEvent` variants on the per-agent bus, so the client doesn't refetch `/api/state` on submit — the SSE stream delivers the new state faster anyway. diff --git a/docs/2-handbook/hives/dashboard.md b/docs/2-handbook/hives/dashboard.md index c7233231c..8a64af06b 100644 --- a/docs/2-handbook/hives/dashboard.md +++ b/docs/2-handbook/hives/dashboard.md @@ -10,7 +10,7 @@ matrix accounts, the controller's job graph — are on the hive-c0re serves the dashboard at `/`. It has a fixed chrome header at the top and a `
` 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 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 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. - `POST /api/purge-tombstone/{name}` — wipe a tombstone's state dirs. - `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 `{ "marked": N }`. Agent name validated against `[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 headers for one agent, newest first. Returns `Vec` (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 stdout+stderr as `text/plain`. Works on both finished and in-progress 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: broker `sent` / `delivered`, plus the mutation events listed @@ -498,12 +498,6 @@ payload): terminal-history trim). Unlike the snapshot events below it ships no state at all: the client re-fetches `GET /api/jobq/graph`, which is where it reads the queue from cold too. -- `reminders_changed` (seq, reminders: `Vec`) — - 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`) — full snapshot of destroyed-but-state-kept agents. Emitted on destroy, purge, and revive. The K3PT ST4T3 tab subscribes. diff --git a/docs/2-handbook/hives/hivectl.md b/docs/2-handbook/hives/hivectl.md index 2d0fe6aaa..70298baa3 100644 --- a/docs/2-handbook/hives/hivectl.md +++ b/docs/2-handbook/hives/hivectl.md @@ -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 paused agent stays paused across a container restart — and pausing a _stopped_ agent makes it come up parked. -- **Not a DAG.** Unlike `restart`/`stop`, there's no container operation - to sequence, so it applies immediately with nothing to wait on. -- **Stopping a paused agent is still fast.** hivectl skips the graceful-stop +- **A DAG, with an acknowledgement wait.** `pause` creates a DAG whose drain + node waits for the harness to acknowledge the marker (giving up after + 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 is safe precisely because the pause check sits at the top of the loop: a paused agent has no turn in flight to checkpoint. diff --git a/docs/2-handbook/swarm/README.md b/docs/2-handbook/swarm/README.md index 195514f96..88d6eaf36 100644 --- a/docs/2-handbook/swarm/README.md +++ b/docs/2-handbook/swarm/README.md @@ -484,9 +484,10 @@ would take delivery away from the first rather than add a recipient. **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 -`/webhook/config-pr` org hook on startup, since a route no hive serves -would otherwise sit on the org collecting failed deliveries. Removing +only one that acts on config PRs. No hive serves `/webhook/config-pr` +any more and none deletes it: a hook of that name left on the org by an +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 next start recreates it.