diff --git a/CLAUDE.md b/CLAUDE.md index 584e21b5..873c107c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -141,6 +141,17 @@ hand-maintained per-file tree drifts out of sync with the code. dependency-free wire types. - **`hive-metric/`** — small CLI to push a single labeled metric to the OTEL collector via the OpenTelemetry Rust SDK / OTLP HTTP exporter. +- **`swarm-controller/`** — swarm-level daemon, opt-in per host + (`services.hyperhive.swarm.controller.enable`). Where `hive-c0re` owns + the agents on **one** host, this owns what is true **across** hives; a + swarm runs one of them, so most hives leave it off. Serves HTTP over a + unix socket (never a TCP port) that the gateway's nginx proxies to. + ⚠️ The socket lives in its **own** `RuntimeDirectory`: it is `0666` + (nginx is a different user and `connect(2)` needs write), so the + containing directory — bind-mounted wholesale into the gateway + container — is the only access control there is. Never move it under a + directory shared with anything else, `/run/hyperhive` (host admin + socket) above all. A unit test pins the path. ### External dependencies with no directory here diff --git a/docs/swarm.md b/docs/swarm.md index addf88f5..b0f166b0 100644 --- a/docs/swarm.md +++ b/docs/swarm.md @@ -235,6 +235,36 @@ one store, because the receiver keys destinations by *agent* so a migrating agent keeps one unbroken incremental chain. See [snapshot-store.md](snapshot-store.md). +## Swarm controller + +`services.hyperhive.swarm.controller.enable` runs the `swarm-controller` +daemon on this host. It is **off by default and deliberately not derived +from `services.hyperhive.enable`**: a swarm has one controller, so +enabling it is a statement about swarm topology, not about whether +hyperhive is installed. Every hive runs `hive-c0re` (which owns the +agents on that host); one hive additionally runs this (which owns what is +true across hives). + +It serves HTTP over a unix socket — `socketPath`, default +`/run/swarm-controller/controller.sock` — rather than a TCP port. The +gateway's nginx is the only intended client and reaches the socket +through a bind-mount, and a listener that is never bound to an address +cannot be reached from off-host by mistake. + +⚠️ **The socket's directory is its access control.** The socket itself is +`0666`, because nginx runs as a different user and `connect(2)` needs +write — the same arrangement hive-c0re uses for the per-agent sockets. +What keeps that safe is that the directory holds one socket and is +bind-mounted into exactly one container. Pointing `socketPath` at a +directory that carries anything else — `/run/hyperhive`, which holds the +host **admin** socket, above all — exposes everything in it to every +consumer that mounts it. Changing `socketPath` therefore means +re-checking the gateway bind-mount, not just the daemon. + +Today the daemon serves a single `/health` endpoint and holds no state: +the unit exists so the swarm-level surfaces that follow have somewhere to +land. + ## Cross-references - `docs/snapshot-store.md` — the swarm's `btrfs receive` endpoint, and