feat(swarmctl): add agent create, queueing the swarm-controller creation DAG

`swarmctl agent create <name> --hive <hive>` POSTs `/api/agents` to
swarm-controller over the daemon's unix socket and prints the queued
job's node id.

It deliberately does not wait. The endpoint queues a DAG whose last node
*publishes* a deploy message; the hive's `hive-c0re` then converges on
its own clock, out of the controller's sight. So even a fully settled
graph would not mean the agent is up, and there is nothing this CLI
could wait for that would let it claim otherwise. Printing the id is
exactly what the response says and all of what it says.

Transport is a bare hyper HTTP/1.1 client handshaked onto a tokio
`UnixStream` via `hyper_util::rt::TokioIo` — the same crate family
`hivectl/src/watch.rs` and `hive-agent/src/web_ui/proxy.rs` already use,
all of it already workspace-pinned. The request/response shapes are a
local mirror rather than a shared crate: the controller's own types are
private to its binary and this crate does not link it, the same
separation `hivectl` keeps from `hive-c0re`.

Errors are reduced to one actionable line — the controller answers
RFC 9457 problem+json, so an unknown `--hive` reaches the operator as
the roster of hives that would have worked rather than a body dump.
Response `warnings` are printed when non-empty.

The nix module wraps the binary with `SWARM_CONTROLLER_SOCKET`, read
from the same `socketPath` the daemon binds.

Refs #4399
This commit is contained in:
atlas 2026-09-14 18:54:14 +02:00 committed by mara
commit 30fa54cbc6
9 changed files with 521 additions and 17 deletions

View file

@ -75,6 +75,14 @@ let
SWARMCTL_AUTHELIA_USERS_FILE = deployCfg.authelia.hostUsersFile;
};
# Where `swarmctl agent create` posts to. Unconditional, unlike
# `autheliaEnv` above: this binary is only installed where the daemon
# runs, so the socket is always the one right below it — and it is the
# same `socketPath` the daemon binds, read once rather than restated.
controllerSocketEnv = {
SWARM_CONTROLLER_SOCKET = deployCfg.swarm-controller.socketPath;
};
forgeCfg = config.services.hyperhive.swarm.forge;
swarmDomain = config.services.hyperhive.swarm.domain;
@ -244,7 +252,9 @@ let
postBuild = ''
wrapProgram $out/bin/swarmctl ${
lib.concatStringsSep " " (
lib.mapAttrsToList (name: value: "--set ${name} ${lib.escapeShellArg value}") autheliaEnv
lib.mapAttrsToList (name: value: "--set ${name} ${lib.escapeShellArg value}") (
autheliaEnv // controllerSocketEnv
)
)
}
'';