hyperhive/swarm-controller/README.md
atlas fb51006717 fix(nix): swarm-controller is not a core binary, and it needed a README
Per review: daemonBins is the core stack, and it drives the bundle that
services.hyperhive.c0re.package points at -- so listing a swarm-scoped
service there would put it in every hive's closure when one hive in a
swarm runs it. It gets the same per-bin extractor, bound on its own, the
way hivectl already is.

The crate also had no README while every other one does. Both misses are
the same shape: adding a thing without updating what describes the set of
things.
2026-08-05 14:59:37 +02:00

60 lines
2.6 KiB
Markdown

# swarm-controller
The **swarm-level** daemon. Where `hive-c0re` owns the agents on one host, this
owns what is true *across* hives — so a swarm runs one of them and most hives
leave it off.
Opt-in per host via `services.hyperhive.swarm.controller.enable`, which is
deliberately **not** derived from `services.hyperhive.enable`: turning it on is
a statement about swarm topology, not about whether hyperhive is installed.
## What it does today
Serves one `/health` endpoint and holds no state.
That is the whole intent of the first slice. The point is to make the *unit*
real — service user, runtime and state directories, socket, nginx
reachability — so the swarm-level surfaces that follow have somewhere to land.
Inventing those surfaces before they are agreed would bake in a shape nobody
chose. See #3066 and the `hyperhive.swarm` consolidation epic.
## Why a unix socket, not a port
The hive-gateway's nginx is the only intended client and reaches the socket
through a bind-mount. A listener that is never bound to an address cannot be
reached from off-host by mistake.
The socket path is `services.hyperhive.swarm.controller.socketPath`, default
`/run/swarm-controller/controller.sock`, exported to the process as
`SWARM_CONTROLLER_SOCKET`.
## ⚠️ The socket's directory is its access control
The socket is `0666`. It has to be: nginx runs as a different user and
`connect(2)` needs write. This matches how `hive-c0re` publishes the per-agent
sockets, and rests on the same argument — *"the bind source dir is per-agent on
host so blast radius is unchanged."*
What keeps that safe is that the directory holds **one** socket and is
bind-mounted into **one** container. So:
> **Never point `socketPath` at a directory that carries anything else.**
> `/run/hyperhive` above all — it holds `host.sock`, the host **admin** socket.
> Mounting that directory to reach this socket would hand the gateway container
> the admin socket along with it.
Changing `socketPath` therefore means re-checking the gateway bind-mount, not
just the daemon. A unit test pins the default path so a tidying edit fails
instead of reviewing cleanly.
`RuntimeDirectoryPreserve=yes` and the daemon's stale-socket unlink on start are
a **pair**: preserving the directory without the unlink means `bind` fails with
`EADDRINUSE` after a restart.
## Packaging
Built by the workspace derivation and extracted as its own package
(`nix build .#swarm-controller`). Deliberately **not** in `nix/packages`'
`daemonBins` — that list is the core stack and drives the bundle
`services.hyperhive.c0re.package` points at, so folding this in would put a
swarm-scoped service into every hive's closure.