Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/docs/swarm/ui.md
iris baf282131f swarm-ui: the review try-out page shows and changes the review classifier's settings
A classifier settings panel under the try-out form reads and saves the model,
effort and how many requested reviews run at once through the settings route.
A save replaces all three; a refused one shows the route's reason and leaves
the form as typed. There is no API key field.
2026-10-13 12:24:47 +02:00

26 KiB

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 hive directory, each hive with its last reported status
/agents every agent: status, config PR, wanted state, inference settings, resource caps; create agents, 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
/jobs the controller's job graph — where agent creation and credential mints show progress
/schedules the scheduled prompts: add, edit, pause, resume and remove
/issues a cross-repo issue report
/review-runs the review classifier's recent runs: which PRs, what it answered, which failed
/review-tryout try a review classification against a real PR

Everything it shows comes from swarm-controller. An agent created here or with swarmctl agent create starts paused; set it up from its card.

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.

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 runs

/review-runs lists what the review classifier 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, 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.

Review try-out

/review-tryout shows how the review classifier 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 shows the review's table: for each classification the verdict, the model's certainty, the threshold, whether the verdict counted and the reason. 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 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.

Classifier settings

Under the try-out form, classifier settings shows the model and effort the review classifier runs with and how many requested reviews run at once, and lets you change them. The model is free text, one word as claude's --model takes it, such as haiku or sonnet. The effort is one of low, medium, high, xhigh and max, or claude'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 replaces all three settings. 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 the 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.

How a message reaches the agent

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.

Where the subagent list and rows come from

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.

Where an agent's terminal history comes from

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.

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 is the subagent model list the swarm sets, or "its own" when it sets none. edit opens a dialog with the three 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. 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 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 sets the hive's own limits.

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.

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.

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 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. An account without that group authenticates fine and still gets bounced.

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.

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.

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.

Adding a swarm service name: the 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 (for example 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 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.

⚠️ 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.

Cross-references