hyperhive/swarmctl
Repository files (latest commit first)
Filename Latest commit message Latest commit date
iris 15890f9d5b docs: fix genuine Microsoft.Contractions/Negative errors
16 of the 23 error-level hits from a full-repo vale run were real:
'is/are/does/do NOT' and bold 'not' expanded per Microsoft.Contractions,
one hyphen-as-negative-number fixed to an en dash per Microsoft.Negative
(docs/web-ui/dashboard.md's -45 deg column-header tilt).

Where the caps/bold was there for emphasis (contrasting X does this,
Y does not), kept the emphasis by bolding the contraction rather than
dropping it silently -- 'is not' -> 'isn't', 'does NOT' -> '**doesn't**',
etc.

docs/tools/swarmctl-cli.md's one genuine hit is generated -- fixed the
source doc comment in swarmctl/src/main.rs and regenerated via
'cargo run --bin swarmctl -- markdown-docs', diffed against the old
copy first to confirm only the intended line changed.

Left 7 hits unfixed, false positives from the rule matching a
substring rather than the actual pattern it names:
- 4x 'that's instead of that is': every one is the explanatory 'that
  is' idiom (~= i.e.), not a contractible subject+verb -- 'that's'
  would misparse as a demonstrative-pronoun contraction and change the
  sentence's meaning. docs/integrations/forge.md:282,
  docs/tools/forge.md:260, docs/tools/hivectl.md:243,
  docs/web-ui/dashboard.md:457 (and the swarmctl-cli.md generated
  twin of the same category, source left alone for the same reason).
- docs/agent-lifecycle/persistence.md:482 -- 'is not' matched inside
  'is nothing' (the same has-not/has-nothing substring trap noted
  before in this doc's own git history).
- docs/swarm/ca.md:182 -- 'it is' matched inside the already-correct
  'it isn't'.

Per #4128. Remaining errors on main after this: Microsoft.Avoid's 26
hits, already tracked + awaiting a house-style ruling on #4041.
2026-09-09 01:48:46 +02:00
..
src docs: fix genuine Microsoft.Contractions/Negative errors 2026-09-09 01:48:46 +02:00
Cargo.toml chore(#3422): drop swarmctl's now-unused serde_json dependency 2026-08-18 10:40:36 +02:00
README.md treefmt: apply prettier 2026-09-02 15:25:07 +02:00

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 there is no socket, no HTTP route and no privileged helper here. 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.

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 (#3422).

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

Usage

# 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.