From 12a26e6cb0daf589a6103af62de0b2b006f72a96 Mon Sep 17 00:00:00 2001 From: damocles Date: Mon, 1 Jun 2026 10:47:09 +0200 Subject: [PATCH] docs: migrate reminder/gateway/crash-watch/migrate prose to docs (#715) --- docs/approvals.md | 25 +++++++++++++++++++++ docs/gateway.md | 29 ++++++++++++++++++++++++ hive-c0re/src/agent_ports.rs | 34 ++++------------------------ hive-c0re/src/crash_watch.rs | 20 ++++------------- hive-c0re/src/gateway_nginx.rs | 31 +++++-------------------- hive-c0re/src/migrate.rs | 24 ++++---------------- hive-c0re/src/reminder_scheduler.rs | 35 ++++------------------------- 7 files changed, 75 insertions(+), 123 deletions(-) diff --git a/docs/approvals.md b/docs/approvals.md index 01736bf6..44d8a122 100644 --- a/docs/approvals.md +++ b/docs/approvals.md @@ -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//state/foo.md`). On delivery hive-c0re: + +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 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, diff --git a/docs/gateway.md b/docs/gateway.md index b1971bff..e974ec2b 100644 --- a/docs/gateway.md +++ b/docs/gateway.md @@ -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//` 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 `.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 diff --git a/hive-c0re/src/agent_ports.rs b/hive-c0re/src/agent_ports.rs index 88f1486b..c856a128 100644 --- a/hive-c0re/src/agent_ports.rs +++ b/hive-c0re/src/agent_ports.rs @@ -1,33 +1,7 @@ -//! `/var/lib/hyperhive/agent-ports.json` writer. Port map for -//! per-agent `/agent//` 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//` 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; diff --git a/hive-c0re/src/crash_watch.rs b/hive-c0re/src/crash_watch.rs index 85feb1e6..ae3ce18e 100644 --- a/hive-c0re/src/crash_watch.rs +++ b/hive-c0re/src/crash_watch.rs @@ -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; diff --git a/hive-c0re/src/gateway_nginx.rs b/hive-c0re/src/gateway_nginx.rs index 1087119e..764545a8 100644 --- a/hive-c0re/src/gateway_nginx.rs +++ b/hive-c0re/src/gateway_nginx.rs @@ -1,30 +1,9 @@ //! Runtime nginx include-file generator for the gateway's per-agent -//! `/agent//` 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 `.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//` 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 _; diff --git a/hive-c0re/src/migrate.rs b/hive-c0re/src/migrate.rs index 5e1ae421..7bebc8cb 100644 --- a/hive-c0re/src/migrate.rs +++ b/hive-c0re/src/migrate.rs @@ -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//.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 --flake meta#` 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; diff --git a/hive-c0re/src/reminder_scheduler.rs b/hive-c0re/src/reminder_scheduler.rs index de370e4f..fd731dfd 100644 --- a/hive-c0re/src/reminder_scheduler.rs +++ b/hive-c0re/src/reminder_scheduler.rs @@ -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//state/foo.md`) to -//! the host path (`/var/lib/hyperhive/agents//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;