hyperhive/swarm-controller
Repository files (latest commit first)
Filename Latest commit message Latest commit date
atlas 465d68d5a7 fix(swarm-controller): bound the token request, and stop claiming a connection
Both from argus's review on the PR, both non-blocking, both real.

The HTTP client had no timeout, and it runs INSIDE the auth callback: a
token endpoint that accepts the connection and then never answers would
hang the callback and the connection attempt that invoked it, with no
retry and nothing in the log to say why. That is the same hang class the
status endpoint's connection-state check exists to prevent, one layer up.
Failing fast lets async-nats back off and try again, which it already
does well.

And the startup log said "connected to the swarm queue" at a point where
`retry_on_initial_connect` guarantees no connection has been established
yet - so the journal would read "connected", then 503 "not connected"
moments later, and a reader would rightly distrust the second line rather
than the first.
2026-08-15 18:46:35 +02:00
..
src fix(swarm-controller): bound the token request, and stop claiming a connection 2026-08-15 18:46:35 +02:00
Cargo.toml feat(swarm-controller): aggregate per-hive status from the swarm queue 2026-08-15 18:37:23 +02:00
README.md docs(gateway): describe what is, not what changed 2026-08-11 18:09:51 +02:00

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. So:

Never point socketPath at a directory that carries anything else. /run/hyperhive above all — it holds host.sock, the host admin socket. Pointing nginx at that directory to reach this socket would put the admin socket within its reach too.

nginx is a host service, so nothing narrows what it can reach except the directory itself — that is the whole of the access control. 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.