Pick agents from a grid of their icons (running or all), type the text, send. The page posts the names and the text to the controller and shows what happened to each agent, with a retry that selects just the ones that failed. The selection rules and the controller's answer are read by plain functions with tests.
574 lines
34 KiB
Markdown
574 lines
34 KiB
Markdown
# Swarm UI
|
|
|
|
The swarm's own web surface and the operator's day-to-day view: served by
|
|
the gateway on the **swarm apex** (`services.hyperhive.swarm.domain`),
|
|
readable only by operators. The per-hive dashboard, on each hive's own
|
|
domain, covers host-level detail for one hive.
|
|
|
|
## What it shows
|
|
|
|
| route | what |
|
|
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| `/` | the start page: the swarm's name over a digital-rain backdrop, plus a paperclip offering to set up an agent while no `ruth` exists (it opens the create form with `ruth` filled in); the swarm name in the header links here |
|
|
| `/hives` | the hive directory, each hive with its last reported status |
|
|
| `/agents` | every agent: status, config PR, wanted state, inference settings, resource caps; create agents (with their permissions, optionally from an [archetype](archetypes.md)), link forge, matrix and GitHub accounts |
|
|
| `/agents/<name>/terminal` | one agent's live terminal |
|
|
| `/agents/<name>/subagents/<subagent>/terminal` | one of that agent's subagents' live terminal |
|
|
| `/alerts` | the alerts the swarm's Grafana holds now (firing, pending, no data, error), read through the controller with a Viewer service-account token |
|
|
| `/jobs` | the controller's job graph — where agent creation and credential mints show progress |
|
|
| `/schedules` | the scheduled prompts: add, edit, pause, resume and remove |
|
|
| `/broadcast` | send one message to several agents at once, picked from a grid of their icons |
|
|
| `/issues` | a cross-repo issue report |
|
|
| `/review` | the review classifier: its recent runs, a try-out on a real PR, and its settings |
|
|
| `/builds` | the swarm builder's recent builds of agent config PRs, with what nix printed when one failed |
|
|
|
|
Everything it shows comes from [`swarm-controller`](../../swarm-controller/README.md).
|
|
An agent created here or with `swarmctl agent create` starts `paused`; set it
|
|
`up` from its card. A card shows **login needed** when the agent's own
|
|
turn-state header says its claude has no session, and a **log in** button
|
|
that runs the claude login from a dialog: open the URL it shows, then paste
|
|
the code back. The credentials go to the agent through the secret store
|
|
([`claude-login.md`](claude-login.md)). The agent's own web UI login still
|
|
works too.
|
|
|
|
### Agent and subagent terminals
|
|
|
|
An agent's detail panel on `/agents` shows a small live preview of its
|
|
terminal, and below it the agent's **subagents**: every subagent the swarm
|
|
queue holds terminal rows for from the last 24 hours. Picking one shows its
|
|
terminal in the same preview, and **expand** opens it in its own tab, as it
|
|
does for the agent. The list refreshes every 10 seconds, so a subagent the
|
|
agent spawns shows up once it writes output.
|
|
|
|
On a terminal's own page (`/agents/<name>/terminal`, or a subagent's) the
|
|
header has an **N running subagents** badge. It opens a list of the agent's
|
|
subagents, each a link to its terminal; on a subagent's page the current one
|
|
gets a marker and the list starts with a link back to the agent's own terminal. A
|
|
subagent counts as running when it published a row in the last 15 minutes. The
|
|
swarm doesn't report more than when the newest row came, so a subagent that
|
|
thinks quietly for longer drops out of the count. The list shows the others below,
|
|
dimmed, as **not running**.
|
|
|
|
An agent's terminal opens with its last 100 rows from the past 24 hours, then
|
|
follows new ones; a reconnect picks up after the last row it showed. A subagent
|
|
terminal shows rows published while it's open, and has no turn-state badges.
|
|
|
|
The settings menu in the header has a **hide debug output** toggle. It drops
|
|
the muted debug rows (thinking, coalesced ticks, harness chatter) from every
|
|
terminal, the preview and the tabs, and rows already on screen hide or show as
|
|
you flip it. The browser keeps the choice for the swarm UI, apart from the
|
|
per-agent pages'.
|
|
|
|
A terminal's own tab takes the agent's icon as its browser-tab icon, so tabs of
|
|
different agents tell apart. A subagent's tab shows its agent's icon. An agent
|
|
with no icon keeps the hyperhive mark.
|
|
|
|
The tab also has an agent badge, with the agent's icon and name, ahead of the
|
|
connection badge. It opens the agent's details in a popover under the badge:
|
|
the same status, wanted state, inference settings, resource caps and linked accounts as the
|
|
panel on `/agents`, without the terminal preview. The popover doesn't dim the
|
|
page. Selecting outside it, or Escape, closes it. You can stop or pause the
|
|
agent, or link an account, without leaving the terminal. A subagent's tab opens
|
|
its agent's details.
|
|
|
|
### Review
|
|
|
|
`/review` is the one page for the [review classifier](review-classifier.md),
|
|
with three sections in a column. **try a classification** and **classifier
|
|
settings** come first, each folded to a single bar: the bar of the settings
|
|
says what they're now, and what you typed in a section stays when you fold it.
|
|
**recent runs** is under them and always open. The sections share nothing and
|
|
ask for their own data.
|
|
|
|
#### Recent runs
|
|
|
|
The **recent runs** section lists what the [review classifier](review-classifier.md) has
|
|
run, newest first, one card per run. A card shows the repo and PR, the head
|
|
commit, whether the run was a try-out or a requested review, how long ago it
|
|
started, how long it took, how many classifications ran, and the outcome as a
|
|
badge. Select a card to open it: it shows the same result table as the
|
|
[try-out](#review-try-out), the overall verdict, the reason when the outcome
|
|
has one, and a link to the posted review when there is one.
|
|
|
|
An outcome can be more than approve or request changes:
|
|
|
|
| badge | what happened |
|
|
| ------------------- | -------------------------------------------------- |
|
|
| `approved` | the run posted an approval |
|
|
| `changes requested` | the run posted a request for changes |
|
|
| `comment only` | there was no clean verdict, so it posted a comment |
|
|
| `queue full` | the queue was full, so it turned the run away |
|
|
| `post failed` | the run finished but posting the review failed |
|
|
| `done` | a try-out finished |
|
|
| `failed` | the run failed; the card says why |
|
|
|
|
The page refreshes every 30 seconds, and a card you opened stays open when it
|
|
does. An empty list says there are no runs yet, and a failed load shows the reason the
|
|
controller gave. The page shows no token counts, cache counts or cost, and
|
|
stores and posts nothing.
|
|
|
|
#### Try a classification
|
|
|
|
The **try a classification** section shows how the [review classifier](review-classifier.md)
|
|
answers a classification on a real PR, before you put it in a repo's config.
|
|
Give it a repo as `owner/name` and a PR number, then write a classification: a
|
|
name, a description, a polarity (positive or negative) and a threshold from 0
|
|
to 100. **add a classification** adds more, and **run** starts the run.
|
|
|
|
The repo field offers the repos that have an open PR, and once you've picked
|
|
or typed a repo, the PR field offers its open PRs by number and title. Both
|
|
fields stay free text: type a repo or a PR the lists lack, such as a merged
|
|
one. If a list can't load, the page shows the reason and the field still takes
|
|
what you type.
|
|
|
|
**load the repo's rules** fills the classifications from the rules file on the
|
|
repo's default branch, so you can run the repo's own rules or start from them
|
|
and edit. It reads the file each time, so a rule merged a minute ago shows up.
|
|
The page names the file and the commit it read, and asks before it replaces
|
|
classifications you have written. If the repo has no rules file, the file
|
|
doesn't parse, or it holds more than ten rules, the page shows the reason and
|
|
leaves your classifications as they were.
|
|
|
|
A run takes a minute or more, and up to half an hour for ten classifications.
|
|
The page says it's running and shows how long it has been, then lists each classification with its verdict, the model's certainty, the
|
|
threshold, whether the verdict counted and the reason; the posted review folds
|
|
those fields into four columns ([`review-classifier.md`](review-classifier.md)). A classification that couldn't run shows its error where the reason
|
|
would be. Under the table are the overall verdict, approve or request changes,
|
|
and the head commit the run read. A failed run shows its error, for example when the swarm
|
|
has no API key yet.
|
|
|
|
What you typed stays after a run, so you can edit the classification and run it
|
|
again. The page saves no classification and no result, and a run posts nothing
|
|
to the forge; like every table in the UI, the result table remembers its column
|
|
filters in the browser. It shows no token counts, cache counts or cost.
|
|
|
|
When a classification answers the way you want, **open a PR** proposes the
|
|
classifications on the page to the repo. The classifier's forge account pushes
|
|
a branch that adds them to the repo's rules file, with you as the commit's
|
|
author, and the page links to the forge's page for opening the PR from that
|
|
branch, where you log in as usual. A classification named like one the file
|
|
already holds replaces it, and the file's other rules stay. It needs no PR
|
|
number. If the forge has no account of yours yet, log in to the forge once
|
|
first. If the classifier's account may not push to the repo, the page says so:
|
|
a repo admin adds it as a collaborator with write access
|
|
([the service account](review-classifier.md#the-service-account)).
|
|
|
|
#### Classifier settings
|
|
|
|
The **classifier settings** section shows the runtime, model and
|
|
effort the [review classifier](review-classifier.md) runs with and how many
|
|
requested reviews run at once, and lets you change them. The runtime is **API
|
|
key** (claude on the Anthropic key) or **ACP** (opencode on a provider). On the
|
|
API key runtime the model is free text, one word as claude's `--model` takes
|
|
it, such as `haiku` or `sonnet`. Choosing ACP shows the provider's fields in
|
|
place of the model: its `https://` URL, its model name, and what it charges in
|
|
USD per million tokens for input and output, with optional cache read and
|
|
cache write prices. The effort is one of low, medium, high, xhigh and max, or
|
|
the runtime's own default. Concurrent reviews is a whole number from 1 to 8;
|
|
try-outs don't count toward it.
|
|
|
|
**save** is on once something changed and the fields are valid. A save changes
|
|
the runtime, model, effort and limit, and the provider too while the runtime is
|
|
ACP, so switching back to API key keeps the stored provider for next time. If
|
|
the controller refuses it, the page shows the reason and leaves the form as you
|
|
typed it. A change takes effect from the next run, with no restart. The page
|
|
has no API key field: admins set each runtime's key in the bao UI.
|
|
|
|
### Scheduled prompts
|
|
|
|
`/schedules` has one card per scheduled prompt. The **name** is the card's
|
|
title, with the **agents** it fires for, the message, how often it repeats and
|
|
when it fires next. **edit** turns the card into its own form; **pause** and
|
|
**resume** stop and restart it; **remove** deletes it after a confirmation.
|
|
**+ schedule** opens an empty form above the list, and the filters narrow the
|
|
list by name, agent or state (`active`, `paused`, or `past` for a one-shot
|
|
whose time has passed). `past` is the controller's clock against the start time,
|
|
not a record that an agent fired, so you can still pause a `past` schedule.
|
|
|
|
A schedule repeats **every** some number of seconds, minutes, hours or days, or
|
|
fires **once**. Its start time is the moment a one-shot fires, and the origin of
|
|
the fire times of a repeating one. The form previews the next three fire times.
|
|
Changing the start or the repeat moves them; editing only the name, message or
|
|
agents doesn't, and the form warns when an edit would.
|
|
|
|
A start in the past fires the latest missed time once, right away, in every
|
|
agent chosen, and the form says so.
|
|
|
|
Only an operator can change a schedule here or with `swarmctl schedule`; an
|
|
agent has no way to add one. The page needs the controller's `/api/schedules`
|
|
routes.
|
|
|
|
### Sending a message to an agent
|
|
|
|
Under an agent's terminal, in the preview and in its own tab, is a **message**
|
|
box. **send** hands the text to the agent's harness, and the agent takes it as
|
|
an inbox message from your authelia username. **sent** means the harness was
|
|
listening and holds the message. If the harness isn't running, or isn't
|
|
connected to the swarm queue, the box shows an error and the agent gets nothing.
|
|
Subagents have no message box.
|
|
|
|
The message doesn't go through the agent's hive, so the hive dashboard's
|
|
message history doesn't list it. The agent's answer shows in its terminal; an
|
|
agent that calls `send` to your username gets an unknown-recipient error. A
|
|
harness that restarts before taking its turn loses the message.
|
|
|
|
<details><summary>How a message reaches the agent</summary>
|
|
|
|
The gateway sets `Remote-User` from authelia's answer on every `/api/`
|
|
request, replacing any the browser sent. `POST /api/agents/<name>/input`
|
|
takes that header as the sender and sends `{from, body}` as a NATS request on
|
|
`$SWARM.input.<name>`. swarm-controller is the only queue client allowed to
|
|
publish there. The agent's harness subscribes to its own subject and queues
|
|
each message for its turn loop with its reminders. Only the agent's own queue
|
|
credential may subscribe there. The harness doesn't reply, so swarm-controller
|
|
answers 202 when the request times out after two seconds, and 503 when the
|
|
queue reports no subscriber.
|
|
|
|
`POST /api/agents/broadcast` takes `{agents, body}` and sends the same
|
|
message to each agent the same way, up to 16 at once. It answers 200 with
|
|
`{results: [{agent, sent, reason?}]}`, one entry per agent in request order,
|
|
and `reason` holds the text the single-agent route would have answered with.
|
|
One agent not listening doesn't stop the others. swarm-controller refuses the
|
|
whole request and sends nothing when `body` or `agents` is empty, when
|
|
`agents` names an agent twice, names more than 64, or names one that
|
|
`GET /api/agents` doesn't list.
|
|
|
|
A command has its own subject, so the harness never mistakes typed text for
|
|
one. `POST /api/agents/<name>/cancel` sends `"cancel"` as a NATS request on
|
|
`$SWARM.control.<name>`, and only swarm-controller may publish there. The
|
|
harness answers `{acted}`, which says whether a turn was running when the
|
|
cancel stopped it. It runs the same code as the hive dashboard's cancel button.
|
|
Unlike input, the harness answers a command, so swarm-controller returns that
|
|
answer as 200, 503 for no subscriber, and 504 when the harness held the command
|
|
and didn't answer within ten seconds. A 504 means unknown, not cancelled.
|
|
|
|
</details>
|
|
|
|
<details><summary>Where the subagent list and rows come from</summary>
|
|
|
|
The agent's subagent daemon publishes each subagent's terminal rows on
|
|
`$SWARM.term.<agent>.sub.<subagent>` with the agent's own queue credential,
|
|
into a stream named `term-sub-<agent>` that swarm-controller creates for every
|
|
declared agent. The stream keeps rows for 24 hours. swarm-controller serves the list as
|
|
`GET /api/agents/<name>/subagents`, the subagents named by the subjects in that
|
|
stream, each with `last_row_unix`, the stream's publish time of its newest row,
|
|
or `null` when that row isn't read within two seconds. The lookups run
|
|
concurrently, sixteen at a time, so the list waits at most two seconds for
|
|
each sixteen subagents.
|
|
swarm-controller also relays one subagent's rows as SSE on
|
|
`GET /api/agents/<name>/subagents/<subagent>/term/stream`. An agent with no
|
|
store identity, or no queue address, publishes no subagent terminals.
|
|
|
|
</details>
|
|
|
|
<details><summary>Where an agent's terminal history comes from</summary>
|
|
|
|
The harness publishes the agent's terminal rows on `$SWARM.term.<agent>`, into
|
|
a stream named `term-main-<agent>` that swarm-controller creates for every
|
|
declared agent, with the same 24 hours or 64 MiB as the subagent stream. That
|
|
stream holds this one subject, and the subagent stream holds only subjects
|
|
below `$SWARM.term.<agent>.sub`, so they never overlap.
|
|
`GET /api/agents/<name>/term/stream` creates a short-lived consumer on it
|
|
that starts 100 rows back, so the SSE stream sends those rows and then each
|
|
new one. Each event's `id` is the row's stream sequence, and a request with
|
|
`Last-Event-ID` resumes after it. An id past the newest row of the stream, as
|
|
after swarm-controller recreates the stream, starts over as a fresh reader. When the
|
|
consumer stops sending its heartbeat, as after a queue restart, the route ends
|
|
the response and the browser reconnects. Until the stream exists, the route sends
|
|
live rows only.
|
|
|
|
</details>
|
|
|
|
### Builds
|
|
|
|
The **builds** page lists the agent config PR heads the swarm builder built,
|
|
newest first, from `GET /api/builder/runs`. Each card shows the repo and PR,
|
|
the first seven characters of the commit, how the build ended (**built**,
|
|
**failed** or **error**), when it started and how long it took. Select a card to
|
|
read it: for a failed build, what `nix` printed; for a built one, the system
|
|
closure; for an error, why no build came back.
|
|
|
|
The **Details** link on a `swarm-builder` commit status opens this page as
|
|
`/builds?run=<id>`, with that build's card open and in view. The builder keeps
|
|
its newest 100 builds, so a link to an older one shows a note instead.
|
|
|
|
### Broadcasting a message
|
|
|
|
The **broadcast** page sends one message to several agents at once. Pick the
|
|
agents from a grid of their icons. **running** shows the agents that report
|
|
themselves running and **all** shows every agent; the browser keeps the choice.
|
|
**select all** ticks every agent the filter shows, and **clear** unticks them.
|
|
The page skips an agent you ticked and then hid with the filter. Type the
|
|
message and press **send to N agents**. The text goes exactly as typed, as an
|
|
inbox message from your authelia username, the same as the box under an agent's
|
|
terminal. One broadcast may name at most 64 agents, and the button stays off
|
|
until you untick the extra ones. A broadcast to many agents can take a few
|
|
seconds to answer, and the button reads **sending…** meanwhile.
|
|
|
|
The page then lists what happened to each agent. An agent that wasn't
|
|
listening shows as not sent, with the reason, and the others still got the
|
|
message. **select the N that failed** ticks just those so you can send the same
|
|
message again. When every agent got the message the box clears; otherwise the
|
|
text stays.
|
|
|
|
### Inference settings
|
|
|
|
The agent's detail panel shows its model and effort under **inference**, each
|
|
with where it comes from: the swarm, its agent.nix, its own pick, or the
|
|
default. Below them are the subagent model list the swarm sets, or "its own"
|
|
when it sets none, and the subagent cap, which reads "no cap" when unset.
|
|
**edit** opens a dialog with the four fields, filled with what the swarm
|
|
stores; saving does a `PUT /api/agents/{agent}/inference` of
|
|
the whole value. A field you leave empty stays out of it, so the agent keeps
|
|
its own value for that field; an empty max subagents is no cap. **clear the swarm's setting** sends the `DELETE`.
|
|
The panel reads `GET /api/agents/{agent}/inference`; before the agent's first
|
|
report reaches the controller, it shows the swarm's stored values only.
|
|
[inference](inference.md) describes the settings themselves.
|
|
|
|
### Resource caps
|
|
|
|
The agent's detail panel shows its CPU quota and memory max under **resource
|
|
caps**. A cap is the swarm's, so it follows the agent to whichever hive runs
|
|
it, and it replaces what the hive says for that field. A field the swarm
|
|
leaves out shows as the hive deciding it. **edit** opens a dialog with the two
|
|
fields, filled with what the swarm stores. A cpu quota is a percentage of one
|
|
core (`400%` is four cores). A memory max is a size such as `512M` or `8G`, a
|
|
percentage of the machine's memory, or `infinity`.
|
|
|
|
Saving does a `PUT /api/agents/{agent}/limits` of the filled fields. A `PUT`
|
|
replaces the stored value whole, so an empty box clears that cap and the hive
|
|
decides that field again. **clear the swarm's caps** sends the `DELETE`. The
|
|
controller checks each value against systemd's own syntax, because systemd
|
|
ignores a value it can't parse and the agent would run uncapped, and it
|
|
refuses, for example, a percentage with a leading zero. The dialog shows the
|
|
refusal's reason under the fields and leaves them as you typed them.
|
|
|
|
A saved change isn't instant. The hive reads the caps about every 30 seconds,
|
|
and a running agent takes a changed cap at its next spawn or rebuild, so the
|
|
row shows the new value before the agent runs under it. The panel reads
|
|
`GET /api/agents/{agent}/limits`. [hivectl](../tools/hivectl.md#per-agent-resource-limits)
|
|
sets the hive's own limits.
|
|
|
|
### Settings at creation
|
|
|
|
The **+ agent** dialog has two collapsed sections, **resource caps** and
|
|
**inference**, with the same fields as the detail panel's editors. Leave the
|
|
fields empty and the request is the plain name and hive; collapsing a section doesn't
|
|
withdraw what you typed in it. Fill them and `POST /api/agents` carries them
|
|
as optional `limits` and `inference` objects, the same shapes the two `PUT`
|
|
routes take. The controller checks them with the `PUT` rules before it queues
|
|
anything, so a bad value gets a refusal with its reason and creates no agent.
|
|
|
|
It stores them ahead of the job graph, before it declares the agent, so they
|
|
are in place before the agent first runs. If the creation then fails, the
|
|
controller puts back what it replaced. Each agent name keys its settings and
|
|
destroying an agent doesn't clear them, so a name set up before keeps its
|
|
stored settings when you leave the fields empty.
|
|
|
|
### Linking external accounts
|
|
|
|
Each agent on `/agents` opens three dialogs that write a credential for it
|
|
into the swarm secret store through swarm-controller. All three are blind
|
|
set actions: no route hands a token back. swarm-controller refuses to link a
|
|
name that already holds an account; delete that account first to replace it.
|
|
|
|
The agent's detail panel lists its linked accounts under **accounts**, one row
|
|
per account: kind, name and host. Opening an agent makes one request,
|
|
`GET /api/hives/{hive}/agents/{agent}/linked-accounts`, which returns every
|
|
account of that agent as names and hosts and never a credential.
|
|
|
|
| kind | one row per | name | host |
|
|
| ------- | ------------------------------------------- | ----------- | ------------------------ |
|
|
| matrix | `swarm/agents/<agent>/matrix/<account>` | the account | its `homeserver`, if set |
|
|
| forgejo | `swarm/agents/<agent>/forge/<label>` | the label | its `url` |
|
|
| github | `swarm/agents/<agent>/github-token`, if any | `github` | `github.com` |
|
|
|
|
The matrix `main` row is the agent's own account, which the swarm mints;
|
|
it carries an **own account** badge. The panel requests the list again when
|
|
a link dialog closes.
|
|
|
|
- **link a matrix account** — `PUT /api/hives/{hive}/agents/{agent}/matrix-accounts/{account}`,
|
|
either a pasted bearer token or a user id + password that
|
|
swarm-controller logs in with, storing the token it gets back.
|
|
- **link a forge account** — `PUT /api/hives/{hive}/agents/{agent}/forge-accounts/{label}`
|
|
with a forge URL and an access token, stored at
|
|
`swarm/agents/<agent>/forge/<label>`. The agent's
|
|
`hive-agent-forge-accounts` unit fetches it into
|
|
`<state>/forge-<label>-token` and `<state>/forge-<label>.json`, the files
|
|
`hive-forge -f <label>` reads. The unit rewrites a pair whenever the
|
|
store's account for its label differs, and never deletes one: a pair whose
|
|
label the store doesn't list stays untouched. swarm-controller answers 409
|
|
to a link for a label that already holds an account, so replacing one
|
|
takes a delete, then a new link.
|
|
- **link a github account** — `PUT /api/hives/{hive}/agents/{agent}/github-account`
|
|
with a personal access token, stored at `swarm/agents/<agent>/github-token`.
|
|
One token per agent. The agent's
|
|
`hive-agent-github-token` unit fetches it into `<state>/github-token`,
|
|
the file its `gh` wrapper, git credential helper and GitHub notification
|
|
poller read. A `github-token` already in place stays when the store holds
|
|
none. What the token needs and how the agent uses it:
|
|
[GitHub accounts](../integrations/github.md).
|
|
|
|
#### Deleting a linked account
|
|
|
|
Every row except the matrix `main` row has a **delete** action. Its
|
|
confirmation names the account and its host, and confirming sends one
|
|
request:
|
|
|
|
| kind | route |
|
|
| ------- | ------------------------------------------------------------------- |
|
|
| matrix | `DELETE /api/hives/{hive}/agents/{agent}/matrix-accounts/{account}` |
|
|
| forgejo | `DELETE /api/hives/{hive}/agents/{agent}/forge-accounts/{label}` |
|
|
| github | `DELETE /api/hives/{hive}/agents/{agent}/github-account` |
|
|
|
|
swarm-controller removes every version of the entry from the swarm secret
|
|
store, and answers 404 when the store holds nothing there. It refuses `main`: the
|
|
swarm mints that account and would re-mint it. The panel requests the list
|
|
again after a delete, including a 404 one: the account being already gone is
|
|
the end state the delete wanted, so the dialog closes without an error.
|
|
|
|
The matrix confirmation has a checkbox, off by default, that logs the token
|
|
out at its homeserver first (`?revoke=true`). A 401 `M_UNKNOWN_TOKEN` from
|
|
that logout means the homeserver already doesn't recognise the token, so the
|
|
delete proceeds as if it had succeeded. Any other failed logout, or an account
|
|
with no homeserver stored, leaves the account in the store and the dialog
|
|
shows why. Deleting a forge account or GitHub token leaves the token valid at
|
|
its provider; revoke it there.
|
|
|
|
The agent isn't told about a delete. Its matrix daemon drops the account when
|
|
it next lists the store, within two minutes. Its forge and GitHub units never
|
|
delete a file, so a token already fetched into `<state>` stays there.
|
|
|
|
Where each credential lives and who reads it:
|
|
[`credentials.md`](credentials.md).
|
|
|
|
## Enabling
|
|
|
|
```nix
|
|
services.hyperhive.deploy.swarm-controller.enable = true; # the UI comes with it
|
|
```
|
|
|
|
The UI has no switch of its own (setting `deploy.swarm-ui.enable` or
|
|
`swarm.ui.enable` has no effect, and warns at evaluation), and doesn't follow `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 build is `deploy.swarm-controller.ui.package` (defaulted from this
|
|
flake's `swarm-ui` package). The older `deploy.swarm-ui.package` still works
|
|
and warns at evaluation.
|
|
|
|
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 one `server_name` don't 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`, followed by a `deny` for everyone else. An
|
|
account without that group authenticates fine and still gets bounced.
|
|
|
|
```sh
|
|
swarmctl user add <you> --email <you>@example.com --group admins
|
|
swarmctl user update <you> --add-group admins # an account that already exists
|
|
```
|
|
|
|
`--email` isn't needed for the UI, but the forge won't create your account
|
|
without one → [setup.md § 2](../getting-started/setup.md#2--your-sso-account).
|
|
|
|
Why a group and not "any session": agents are authelia subjects too, so
|
|
_authenticated_ includes every agent in the swarm. The group is the only
|
|
thing standing between "an operator's page" and "anyone with a session."
|
|
|
|
## What it costs to be reachable
|
|
|
|
hyperhive publishes the apex to the hive's resolver like every other
|
|
swarm service, so agent containers can resolve it. That's deliberate and it's
|
|
not a hole: **reachability isn't the access control here.** An agent
|
|
that resolves the name and connects still has no operator session, and
|
|
the subrequest denies it.
|
|
|
|
## 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. 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 `services.hyperhive.agent.dashboardLinks`).
|
|
|
|
Links to swarm services come from swarm-level options, so the list is the
|
|
same whichever host runs each service:
|
|
|
|
| link | URL built from |
|
|
| -------- | ------------------------------------------------- |
|
|
| Forge | `services.hyperhive.swarm.forge.domain` |
|
|
| Authelia | `services.hyperhive.swarm.authelia.domain` |
|
|
| Grafana | `services.hyperhive.swarm.grafana.domain` |
|
|
| Metrics | `services.hyperhive.swarm.victoriametrics.domain` |
|
|
| Logs | `services.hyperhive.swarm.victorialogs.domain` |
|
|
| Matrix | `services.hyperhive.swarm.matrix.gatewayHost` |
|
|
| Bao | `services.hyperhive.swarm.bao.ui.domain` |
|
|
|
|
Each entry is present whenever its option has a value — every option in
|
|
the table defaults to one, so the link appears even on a swarm that runs
|
|
no instance of that service.
|
|
|
|
`nix/host-modules/swarm-controller.nix` builds these entries. `swarm-ui.nix`
|
|
adds one more, for this UI's own API docs. An operator can add entries
|
|
directly. An empty list hides the button.
|
|
|
|
The **Matrix** entry opens the swarm's matrix web client (fluffychat,
|
|
`services.hyperhive.deploy.matrix.gui.package`) at the homeserver's
|
|
gateway host, `chat.<swarm domain>` by default. The homeserver's host
|
|
always serves the client at `/` on that vhost, so the entry is present
|
|
whenever the swarm names a gateway host.
|
|
|
|
The **Bao** entry opens the secret store's web UI, which admits members of
|
|
authelia's `admins` group only; see [`secrets.md`](secrets.md#the-browser-ui).
|
|
|
|
<details><summary>Adding a swarm service name: the two wiring sites</summary>
|
|
|
|
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 (for example `nix/host-modules/swarm-ui.nix`) |
|
|
| **certificate name** | `nix/host-modules/swarm.nix` (`serviceDomains`) |
|
|
|
|
<!-- vale write-good.Passive = NO -->
|
|
|
|
The DNS record and the local-dev `/etc/hosts` entry need no separate
|
|
edit: both derive 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.
|
|
|
|
<!-- vale write-good.Passive = YES -->
|
|
|
|
⚠️ The certificate one is the hardest to predict and the most visible when
|
|
missed. `serviceDomains` is _both_ the `allowed_domains` the secret
|
|
store's `pki/roles/swarm-services` narrows to and the list each gateway's
|
|
leaf draws its SANs from (it carries the ones that host fronts),
|
|
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.
|
|
|
|
</details>
|
|
|
|
## Cross-references
|
|
|
|
- [`sso.md`](sso.md) — the authelia instance itself, and the user store.
|
|
- [`../networking/gateway.md`](../networking/gateway.md) — the full vhost map and TLS modes.
|