Per mara's go-ahead on hyperhive#3902 ("getting started is good, but
terminal rendering does not go in there i think"):
Moved 21 top-level docs/*.md files into 7 new topic subdirectories
(existing web-ui/, turn-loop/, swarm/, tools/, crates/ untouched):
getting-started/ setup.md
agent-lifecycle/ agent-hierarchy.md, approvals.md, persistence.md
trust-boundary/ boundary.md, security.md
integrations/ forge.md, matrix.md, github.md, knowledge.md
networking/ gateway.md, network.md, snapshot-store.md
scheduler/ jobq.md, coordinator.md, ci.md, observability.md
process/ conventions.md, gotchas.md, pr-review-gate.md
web-ui/ terminal-rendering.md (moved into the EXISTING dir,
per mara's correction to the original getting-started
guess -- it's UI implementation detail, not onboarding)
The physical layout now matches docs/README.md's own topical headers,
which already amounted to this taxonomy -- see the scoping comment on
the issue for the two findings that motivated this (a genuine
duplication between CLAUDE.md's old "Reading paths" list and
docs/README.md's grouped one, since drifted out of sync with each
other; and the flat layout not matching the grouping we already had).
Fixed every cross-reference this moved across the whole repo (~120
files: docs/ internal links at every depth, Rust doc comments, nix
module option docs, crate READMEs) -- verified two ways: a grep sweep
confirming zero remaining references to any old path, and a script
that resolves every markdown link in docs/**/*.md + CLAUDE.md +
README.md against the filesystem and reports anything that doesn't
exist (zero broken links).
Collapsed CLAUDE.md's "Reading paths" section (the duplicate) down to
a pointer at docs/README.md, now the single index. Rewrote
docs/README.md itself to use the new subdirectory paths and added the
one doc it was missing that CLAUDE.md's old copy had (pr-review-gate.md).
Classified all 22 docs/*.md files first via a haiku subagent (mara's
suggestion) on two axes -- proposed grouping and operator-vs-
implementation focus -- before finalizing the taxonomy; spot-checked
the report and found internal inconsistencies (its classification
table disagreed with its own summary section for a few files), so this
taxonomy is my original proposal + the one correction mara gave
directly, not a blind application of the subagent's table. The
operator-focus data it gathered is still useful for a follow-up
content pass (docs skewing 'mixed' rather than pure operator-facing),
not addressed in this PR -- structure only.
nix fmt clean, both pre-push lints clean.
5.1 KiB
Swarm UI
The swarm's own web surface, served by the gateway on the swarm apex
(services.hyperhive.swarm.domain) and readable only by operators.
Distinct from the per-hive dashboard, which lives on the hive domain and answers for one host. This one is the view across hives.
Enabling
services.hyperhive.deploy.swarm-ui.enable = true; # defaults to deploy.swarm-controller.enable
Derived from the controller rather than from allSwarmServices:
the UI is a view onto the controller's state and reaches it over that
daemon's socket, so the host that runs the controller is the host that
can serve the UI. A hive that merely uses a swarm has nothing to serve.
The UI answers on services.hyperhive.swarm.domain and nothing else. It
shares that name with the swarm-controller it fronts — one service to a
reader and to a certificate — so there is no separate option to pin.
The apex must differ from
services.hyperhive.domain. The gateway's default server already answers for the hive domain, and two vhosts claiming oneserver_namedo not error — nginx picks one — so this is an assertion rather than a runtime surprise.
🔑 You must be in the admins group
This is the step that separates "protected" from "locked out". The
vhost's auth_request asks authelia "is there a session"; the rule
that makes it mean "is this an operator" is an access_control entry
requiring group:admins. An account without that group authenticates
fine and still gets bounced.
swarmctl user add <you> --group admins
admins deliberately, not a new word: ../getting-started/setup.md has
told every operator to create exactly that group since the bootstrap step
existed, so an account made by following the guide already passes. This
is the first rule that consumes a group name — inventing a second one
would have meant those accounts silently failing a check they were
supposed to pass.
An account created without any group needs re-adding with the flag —
swarmctl reads the existing entry out of users.yml, so the group is
what changes.
Why a group and not a list of usernames: agents are getting authelia accounts of their own (matrix SSO), and authenticated would then include every agent in the hive. The group is the only thing standing between "an operator's page" and "anyone with a session".
What it costs to be reachable
The apex is published to the hive's resolver like every other swarm service, so agent containers can resolve it. That is deliberate and it is not a hole: reachability is not the access control here. An agent that resolves the name and connects still has no operator session, and the subrequest denies it.
Two wiring sites
Adding a swarm service name means touching two things. Missing the second ships as a different flavour of "works from the host, broken from a container":
| site | file |
|---|---|
vhost + gateway.localNames |
the service's own module (e.g. nix/host-modules/swarm-ui.nix) |
| certificate name | nix/host-modules/swarm.nix (serviceDomains) |
The DNS record and the local-dev /etc/hosts entry need no separate
edit: both are derived from services.hyperhive.gateway.localNames,
which a service's own module already has to push its domain into to be
resolvable — see nix/host-modules/hive-gateway/dnsmasq.nix and
.../default.nix's networking.hosts. vhosts.nix itself is scoped to
the surface the hive's own domain serves (dashboard, per-agent routing,
matrix discovery); a swarm service declares its own vhost next to its
own options, the way swarm-ui.nix and swarm-authelia.nix do.
⚠️ The certificate one is the least obvious and the most visible when
missed. serviceDomains is both the services sub-CA's
nameConstraints set and the leaf's SAN list, and the apex is a
sibling of forge.<swarm> / chat.<swarm> / auth.<swarm>, not a
parent — no CA in the hierarchy issues for it implicitly. Left out, the
vhost falls back to the hive leaf and the swarm's front page opens with a
name mismatch.
Quick links
The swarm UI's header carries a single 🔗 button, visible on every route,
opening a popover of links to other swarm-wide services — authelia,
matrix, forge, this UI's own swagger docs. Backed by GET /api/links
(swarm-controller), which serves services.hyperhive.swarm.controller.links
(a listOf { label, icon, url }, same shape as the per-agent
hyperhive.dashboardLinks).
Rather than one central hardcoded list, each service's own module
contributes its own entry when it's actually enabled on the controller's
host — swarm-authelia.nix, hive-matrix.nix and hive-forge/default.nix
all do, the same list-merge idiom gateway.localNames uses above. Adding a
link for a new service is a nix-only change to that service's own module
(or an operator adding an entry directly); no swarm-controller or swarm-ui
change needed. Empty list hides the button rather than showing an empty
popover.
Cross-references
sso.md— the authelia instance itself, and the user store.../networking/gateway.md— the full vhost map and TLS modes.