docs: migrate reminder/gateway/crash-watch/migrate prose to docs (#715)

This commit is contained in:
damocles 2026-06-01 10:47:09 +02:00
commit 12a26e6cb0
7 changed files with 75 additions and 123 deletions

View file

@ -162,6 +162,31 @@ Transient broker errors (sqlite lock contention, etc.) get the same
fan-out pass; a broker error on a one-shot is not retried (the
operator advisory and `last_result` are the only audit trail).
### Reminder delivery: file-path semantics
A reminder may carry a `file_path` (the agent-visible path inside its
container, e.g. `/agents/<name>/state/foo.md`). On delivery hive-c0re:
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 the write is skipped and the original message is
delivered inline with a warning — the reminder still fires.
3. **Defends against symlink escape**: after `create_dir_all`, the parent
dir is canonicalized and re-verified to live under the agent's host
state root. The final file is opened with
`O_NOFOLLOW | O_CREAT | O_TRUNC` so an existing symlink at the
basename cannot 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 bulky
payload is read out of band.
Atomicity of the inbox INSERT + `reminders.sent_at` UPDATE is handled
inside `Broker::deliver_reminders_batch`; the scheduler only computes the
body strings before calling it.
### Destroy semantics
`HostRequest::Destroy { name, purge }` is the lifecycle tear-down,

View file

@ -114,6 +114,35 @@ TCP loopback upstream in `agents.conf` (deterministic port from
`agent_web_port(name)`). A future cleanup will drop the TCP fallback
once every agent's flipped.
## Agent port map (`agent-ports.json`)
`/var/lib/hyperhive/agent-ports.json` is a flat JSON object keyed by
logical agent name → TCP web port:
```json
{
"iris": 8178,
"atlas": 8304,
"argus": 8267,
"damocles": 8549
}
```
Written alongside `agents.conf` on every topology change. Ports come from
`lifecycle::agent_web_port(name)` — a pure FNV-1a hash of the name,
reproducible from the name alone. The manager is excluded: the gateway
routes `/` directly to c0re's dashboard upstream, not through a
per-agent `/agent/<name>/` prefix.
The file doubles as a human-readable audit artifact — `cat agent-ports.json`
shows every registered sub-agent and its deterministic port assignment. TCP
loopback upstreams in `agents.conf` reference these ports for agents that
haven't opted into unix-socket mode yet.
Both `agent-ports.json` and `agents.conf` use atomic `<path>.tmp` +
`rename()` writes so a crashing c0re process never leaves a partial or
unparseable file behind.
## Dashboard link shape (gateway vs direct)
When the gateway is in front, the SW4RM tab builds per-agent links

View file

@ -1,33 +1,7 @@
//! `/var/lib/hyperhive/agent-ports.json` writer. Port map for
//! per-agent `/agent/<name>/` TCP routing. Written alongside
//! `agents.conf` (see `gateway_nginx.rs`) on every topology change;
//! `gateway_nginx::render` reads it indirectly via
//! `lifecycle::agent_web_port` to populate TCP upstreams for agents
//! that haven't opted in to unix-socket mode yet. Also kept as a
//! human-readable audit file — `cat agent-ports.json` shows every
//! registered sub-agent and its deterministic port assignment.
//!
//! Shape (flat object keyed by logical agent name → web port):
//!
//! ```json
//! {
//! "iris": 8178,
//! "atlas": 8304,
//! "argus": 8267,
//! "damocles": 8549
//! }
//! ```
//!
//! Ports come from [`crate::lifecycle::agent_web_port`] — pure
//! FNV-1a(name) hash so the value is reproducible from the name
//! alone. Manager is excluded from the map: the gateway routes `/`
//! straight to it via the c0re dashboard upstream rather than a
//! per-agent `/agent/<name>/` prefix.
//!
//! Atomicity: write to a sibling `.tmp` file + rename so a partial
//! write never leaves an unparseable file in place. The gateway's
//! `nginx` worker can read mid-write and Just Work because `rename()`
//! is atomic on the same filesystem.
//! `/var/lib/hyperhive/agent-ports.json` writer — flat map of
//! agent name → TCP web port. Written alongside `agents.conf` on
//! every topology change. JSON shape, port derivation (FNV-1a hash),
//! atomicity, and manager exclusion: `docs/gateway.md::Agent port map`.
use std::collections::BTreeMap;
use std::path::PathBuf;

View file

@ -1,19 +1,7 @@
//! Per-container state watcher. Polls every managed container on a
//! fixed interval, tracks two orthogonal state-sets across ticks,
//! and emits a `HelperEvent` to the manager on each transition:
//!
//! - **running**: container is up. running → stopped without an
//! operator-initiated transient (`Stopping` / `Restarting` /
//! `Destroying` / `Rebuilding`) → `ContainerCrash`.
//! - **logged-in**: claude session dir is populated. ! → ✓ →
//! `LoggedIn`; ✓ → ! → `NeedsLogin` (rare — usually only fires
//! on a fresh spawn / purge).
//!
//! `NeedsUpdate` events are now fired from the apply-commit path
//! directly rather than via rev-marker polling.
//!
//! D-Bus subscription would be lower-latency for the first axis,
//! but polling is simpler and a 10s detection delay is fine.
//! Per-container crash and login-state watcher. Polls every managed
//! container on a 10s interval. Fires `ContainerCrash`, `LoggedIn`,
//! and `NeedsLogin` helper events. Event semantics and the
//! `RECENT_TRANSIENT_GRACE` window: `docs/approvals.md::Helper events`.
use std::collections::HashSet;
use std::sync::Arc;

View file

@ -1,30 +1,9 @@
//! Runtime nginx include-file generator for the gateway's per-agent
//! `/agent/<name>/` location blocks.
//!
//! Writes `/var/lib/hyperhive/gateway/agents.conf` on every topology change.
//! The gateway container bind-mounts `/var/lib/hyperhive/gateway/` (NOT the
//! whole parent dir) at `/run/hive-state/`; nginx includes
//! `/run/hive-state/agents.conf`. After each write, c0re triggers
//! `nginx -s reload` inside the gateway container via
//! `systemd-run --machine=hive-gateway` from the host — no `nixos-rebuild
//! switch` needed when agents start, stop, or flip `useUnixSocket`.
//! (A path unit inside the container was tried first but `IN_MOVED_TO`
//! from the atomic rename does not cross the nspawn mount-namespace
//! boundary — see `docs/gateway.md` for the failure analysis.)
//!
//! Upstream selection mirrors `agent_sockets::build_map`: an agent
//! gets a UDS upstream when its `.bound` marker exists (harness has
//! bound the unix socket); otherwise falls back to the deterministic
//! TCP port from `lifecycle::agent_web_port`. Proxy headers are
//! emitted in full so the generated file is self-contained nginx
//! config — no dependency on which `recommendedProxySettings` knobs
//! the host config has on.
//!
//! `write()` is idempotent: if the rendered body equals what's already
//! on disk, the rename is skipped and the path unit doesn't fire.
//! Same atomic `<path>.tmp` + `rename()` shape as `agent_ports` /
//! `agent_sockets` — a crashing c0re process never leaves a partial
//! file the gateway's nginx would fail to parse.
//! `/agent/<name>/` location blocks. Writes
//! `/var/lib/hyperhive/gateway/agents.conf` on every topology change.
//! UDS vs TCP upstream selection, reload trigger (`systemd-run
//! --machine=hive-gateway`), and idempotency:
//! `docs/gateway.md::Per-agent unix-socket upstream`.
use anyhow::{Context, Result};
use std::fmt::Write as _;

View file

@ -1,23 +1,7 @@
//! Startup auto-migration from the pre-meta layout. Runs before
//! `auto_update::run` and consists of four phases, each idempotent:
//!
//! 1. Per-agent applied repo: rewrite `flake.nix` to the module-only
//! boilerplate if it isn't already, commit, relocate `deployed/0`
//! to HEAD so `setup_applied`'s existence check passes.
//! 2. Per-agent proposed repo: ensure the `applied` git remote
//! points at `/applied/<n>/.git` (re-runs `setup_proposed`'s
//! `ensure_applied_remote` indirectly via a host-side git call).
//! 3. Meta repo: `meta::sync_agents` over the current agent list —
//! init the repo on first call, rerender + relock if anything
//! drifted.
//! 4. Container repoint: for every existing container, run
//! `nixos-container update <c> --flake meta#<name>` so it
//! activates against the meta flake. Guarded by a marker file
//! so the (expensive) phase 4 only runs once across hive-c0re
//! restarts.
//!
//! Env kill-switch: `HIVE_SKIP_META_MIGRATION=1` skips the whole
//! migration. Use when smoke-testing one agent at a time by hand.
//! Startup auto-migration from the pre-meta layout. Four idempotent
//! phases: applied repo, proposed repo, meta repo, container repoint.
//! Kill-switch: `HIVE_SKIP_META_MIGRATION=1`. Full migration sequence
//! and phase details: `docs/approvals.md::Migration from the pre-tag`.
use std::path::{Path, PathBuf};
use std::sync::Arc;

View file

@ -1,34 +1,7 @@
//! Background loop that drains due reminders out of the broker and
//! delivers them as inbox messages. Mirrors the `events_vacuum` /
//! `crash_watch` shape — a single `spawn(coord)` entry point started
//! from `main.rs`.
//!
//! File-path semantics: a reminder may carry a `file_path` (the
//! agent-visible path inside its container). On delivery we:
//!
//! - Translate the container path (`/agents/<agent>/state/foo.md`) to
//! the host path (`/var/lib/hyperhive/agents/<agent>/state/foo.md`)
//! so hive-c0re can write to it from outside the container.
//! - Reject anything that isn't under the agent's own state subtree,
//! contains `..` (path traversal), or has an empty relative tail.
//! Falling outside the allowed prefix means the file write is
//! skipped and the original message is delivered inline (with a
//! noted warning) — the reminder still fires, just without the
//! payload split.
//! - Defend against symlink escape: after `create_dir_all`, the
//! parent dir is canonicalized and re-verified to live under the
//! agent's host state root. Then we open the final file with
//! `O_NOFOLLOW | O_CREAT | O_TRUNC` so an existing-symlink basename
//! can't redirect the write either. Without this an agent could
//! `ln -s /etc /agents/foo/state/escape` and bounce a write to an
//! arbitrary host path.
//! - Write the reminder body to disk and deliver a short pointer
//! message in its place, so the agent's inbox/wake-prompt stays
//! small and the bulky payload can be read out of band.
//!
//! Atomicity of the inbox INSERT + `reminders.sent_at` UPDATE is handled
//! inside `Broker::deliver_reminders_batch`; this module only computes the
//! body strings before calling it.
//! Background loop that drains due reminders from the broker and
//! delivers them as inbox messages. 5s poll cadence, shutdown-aware.
//! File-path semantics (path translation, traversal + symlink defense,
//! pointer delivery): `docs/approvals.md::Reminder delivery`.
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;