Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/swarmctl/README.md
atlas 0cbb7db2c0 docs: a first SSO login makes a human's forge account
setup.md said Swarm SSO creates the operator's forge account, which was
not true until the previous commits. It now says how: sign in to the
forge once through authelia, then `swarmctl forge make-admin <you>`.
sso.md says what that first login does and why ACCOUNT_LINKING is
`login`. README, hivectl.md and forge.md drop `hivectl forge
create-user`, and the swarmctl README gains `forge make-admin`.

Refs #3782
2026-09-25 08:29:56 +02:00

125 lines
5.8 KiB
Markdown

# swarmctl
Swarm-level operator CLI. Runs as **root on the host running
`swarm-controller`**, and acts on that host directly.
Distinct from `hivectl`, which drives one hive's `hive-c0re` over its
admin socket. This crate does not link `swarm-controller`, for the same
reason `hivectl` does not link `hive-c0re`.
## Why root, and why no socket
The first verb writes authelia's users database. Making that write
rootless was examined and rejected — **relocating the file only turns a
write problem into a read problem.** Move `users.yml` into a directory
the controller owns and the controller can write it, but authelia then
has to read it across the same boundary in the other direction. Making
_that_ work needs either a hand-pinned gid (the container's uids are
allocated inside it, at activation — see the uid-assignment issue) or
world-readable password hashes. Both are worse than root.
So this binary **serves** no socket, publishes no HTTP route and has no
privileged helper. When a verb has to run as a non-root user or from
another host, the answer is a **group-gated admin socket**, separate from
the controller's `0666` gateway-facing one — not a widening of what root
does here.
That rules out a way in, not a way out: `agent create` is a _client_ of
the controller's socket, because the work it asks for is a job graph only
the controller can queue (see below). Nothing here becomes reachable by
that.
## One file, two writers
`users.yml` — authelia's own users database — is read and written
directly. There is no second store.
There used to be: a private `users.json` here, canonical, with `users.yml`
rendered from it, while `swarm-authelia-bridge` kept its own pair against
the _same_ physical file. Two canonical stores for one file is a seam, and
it bit — a writer whose own JSON was missing could not tell "nothing here
yet" from "someone else's users", and refused to write.
The argument for the split was that it let this crate work without a YAML
parser. It didn't: the JSON was read back on every run, so the round-trip
was already being paid — the two files differed only in _format_.
⚠️ The file is round-tripped, so **comments and hand-formatting do not
survive a write**. Values do, and so do keys this binary does not model.
See `swarm-authelia-bridge/README.md` for what both writers must uphold.
## Configuration
Every path comes from the nix module that installs the binary, because
every one is derived from an option that module owns. They are required
rather than defaulted — a default would be an address we _hope_ points at
something, and one that resolves cleanly to the wrong place is worse than
an error.
| variable | what |
| ------------------------------ | -------------------------------------------------------------------- |
| `SWARMCTL_AUTHELIA_BIN` | the **configured** authelia; argon2 params must match the verifier's |
| `SWARMCTL_AUTHELIA_USERS_FILE` | host-side path of the users database |
| `SWARMCTL_AUTHELIA_MACHINE` | container name, for `systemctl -M` |
| `SWARMCTL_AUTHELIA_UNIT` | authelia's unit inside that container |
| `SWARM_CONTROLLER_SOCKET` | the controller's unix socket, for `agent create` |
## `user add`
```console
# swarmctl user add mara --display-name "Mara" --group admins
```
The password is **generated by authelia** (`crypto hash generate argon2
--random`) and printed once. It is never passed on a command line:
`/proc/<pid>/cmdline` is world-readable, so a password in argv is readable
by any local process for the lifetime of the call.
## `agent create`
```console
# swarmctl agent create scribe --hive alpha
queued: job node 42
agent "scribe" will be deployed to hive "alpha" once the job graph runs; `swarmctl` does not wait for it
```
`POST /api/agents` on the swarm-controller, over its unix socket
(`--controller-socket`, else `SWARM_CONTROLLER_SOCKET`, which the nix
module sets from the daemon's own `socketPath`). It prints the queued
job's node id **and stops there**.
It deliberately does not wait. The endpoint queues a DAG — SSO identity,
forge user, forge repo, repo membership, config-repo seed, then a deploy
message — and the last of those _publishes_: the hive's `hive-c0re` picks
it up and 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 say so honestly. Watch
the swarm UI's job view for the rest.
No approval gate, for the same reason nothing else here has one: running
this binary already means being root on the controller's host.
`--hive` is required. It is an _address_ — where the deploy message goes
— not an attribute of the agent, so there is no sensible default. The
controller checks it against the swarm's hive roster and names the hives
that would have worked when it misses.
The request and response shapes are mirrored in `src/agent.rs` rather
than shared: the controller's own types are private to its binary, this
crate does not link it, and there is no wire-type crate between them.
Two fields out, two in, both ends validating — a drift shows up as a
400 naming the field.
## `forge make-admin`
```console
# swarmctl forge make-admin mara
forge: "mara" is now a site admin
```
`POST /api/forge/users/{name}/admin` on the swarm-controller, over the same
socket as `agent create`. It never creates an account: the forge makes a
person's on their first login through authelia, and until then this fails
saying so. Running it on a site admin changes nothing, and an agent's name is
refused. The response shape is mirrored in `src/forge.rs`, for the same
reason as `agent create`'s.