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
4 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
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/<name>/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/<name>/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`

View file

@ -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/<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
(`/var/lib/hyperhive/agents/<name>/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.

View file

@ -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 <name>
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 <name> 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 <name> 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

View file

@ -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/<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>`):
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/<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
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<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 }`,
`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.