49 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 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, permissions; create agents (with their permissions, optionally from an archetype), 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: the firing ones, with a button for the pending, no-data and error ones and a link to Grafana's own alert list; 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 |
/messages |
the swarm-wide message feed: every message between agents, and to them from the operator, reminders and compaction, with a history window and a filter on sender or recipient |
/notifications |
the notification router's flows: a switch for each built-in, every flow drawn as a graph, and an editor for custom flows |
/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.
Pages that refresh on a timer share one polling rule: the page sends no new
request while the last one is still running, so an older answer never overtakes
a slow one, and a failed refresh keeps the rows already on screen under an
error.
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). 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. A subagent that drops out of the
list closes its terminal, and a later subagent with the same name starts
unpicked.
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 does the same with that subagent's rows, and has no turn-state badges. The browser retries a dropped connection by itself. When the swarm answers the stream with an error status, the browser gives up and the badge reads disconnected. The reconnect button beside it opens the stream again, and the stream replays the rows. The messages page works the same way.
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, permissions 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. From the keyboard, opening a
popover moves focus into it, and Escape puts focus back on the badge. The filter
popovers on table headers work the same way. 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,
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 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.
Try a classification
The try a classification section 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 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). 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).
Classifier settings
The classifier settings section shows the runtime, model and
effort the review classifier 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.
The form offers the agents of the roster that /agents lists. The page reloads
that roster with the schedules, so an agent created after you opened the page
shows up without a reload. If the roster fails to load, the form says so instead
of claiming there are no agents.
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.
Notification flows
/notifications lists the notification router's flows
in two groups. Each built-in flow has an on/off switch; switching one off
stops the router running it from its next event, and switching it on again
restores it. Each custom flow shows its name and whether it's on. Every
flow draws as a graph, the one the editor shows but read-only: the forge events
it starts from on the left, the conditions after them, and the todo or review
request each branch ends in. A box cuts a long line of text with an ellipsis;
hover over the box to read all of it.
A built-in flow has a duplicate to edit button and a custom flow has edit and duplicate buttons. create flow, at the top of the page beside the refresh interval, starts a custom flow with just a source. Creating and duplicating open the editor on a draft and make nothing: the flow exists once you save it there. Leaving the draft without saving, or discarding it, leaves the flows as they were.
A switch sends the whole flows document with the revision the page read, 0 if
it read no document. The controller refuses the switch when the stored revision
is another, including when someone wrote a document after the page read none, and
the page shows why. Reload to switch against the current document.
Each box in a graph says how many of the router's newest 50 forge events reached
it (12 hits, say), from a dry run of the saved flows (GET /api/notify/dry-run).
Below the flows, recent deliveries lists what the router delivered, newest
first: when, which flow and node, the event and issue, who got it, and whether it
went through (GET /api/notify/deliveries). Both come from the router; while the
running router is older than the controller, each shows the controller's "not
available" message in place of its numbers or table, and the rest of the page
works. See deliveries and dry runs.
Editing a flow
/notifications/<flow id> edits one custom flow, and /notifications/draft/new makes
one: it opens on an empty flow, or on a copy of a flow with ?from=<flow id>, as
create flow and duplicate do. A new flow takes its id from its name when
you save it, and save needs a name. The canvas draws the flow left
to right: the source on the left, each match to its right, and the leaf nodes at
the ends. Pick a node to edit it in the panel beside the canvas:
- a source picks the forge events the flow starts from;
- a match lists conditions, each an event field and the value it must equal;
- a todo says who gets it (the agents an event field names, or one agent, picked from the agents the swarm has),
whether it sets or clears a todo, the todo's key, and its text. The key and the
text can use
{field}placeholders; - a request_reviewer names the forge login the router asks to review the pull request. It ends a branch, like a todo, and the row of kinds above the canvas only offers it when the controller does.
Rewire the flow on the canvas:
- Add a node from the row of kinds above the canvas. Choose a kind to add it below the picked box, or drag it onto any box.
- Move a node and its branch by dragging its box onto another box. With the keyboard, the arrow keys walk the tree, M picks up the focused box, the arrows go to the box you want it under, and Enter drops it there. Escape puts it back. The panel's move under list does the same.
- Reorder the branches of a node. The router runs a node's branches in the order they sit, so when a clear and an upsert share a key prefix, the order decides what's left. A box with siblings says when it runs, such as "runs 2nd". run earlier and run later in the panel, or Alt+Up and Alt+Down on the canvas, swap it with its neighbour.
- Remove a node and its branch with remove in the panel or the Delete key.
The editor turns away any edit the controller would refuse to save, and says why: a leaf node with children, a second parent, a node under its own branch, or more nodes or levels than the controller allows. A drop it will refuse shows the target box dashed and red while you hold the box over it.
Edits stay in a draft until save, which sends the whole flows document with the revision the editor read. discard edits goes back to the saved flow and delete flow removes it, after a confirmation. If someone changed the flows elsewhere in the meantime, the controller turns the save down as stale, and the editor offers to reload and drop your edits, or to keep them and replace this flow's latest version with them. For any other refusal the editor shows the controller's own message. The router runs a saved flow from its next event.
Where the controller's schema knows the values, the editor offers them instead of
asking you to type a name: kind and event conditions and the todo's op are
dropdowns, action and repo conditions and a reviewer are boxes that list the
known values to pick from and still take any other text (the repositories listed
are those with an open issue, so one with none is missing), and a todo's agent is a
dropdown of the swarm's agents. A value set in a flow that the list no longer
holds stays selected, marked "not listed." If a list fails to load, or the
controller predates the schema, the box is plain text. The values come from the
controller's schema (notify-router), so a new one shows up
without a UI change.
preview runs the draft, saved or not, over the newest forge events and shows on each box how many of them reached it, as a dry run does for the saved flows in the list. It needs no save, so you can try a half-built flow. The numbers go as soon as you edit the flow. If the draft would not save, or the router can't run a dry run yet, the controller's own message shows instead.
Leaving the page with unsaved edits asks first, whether you close or reload the tab, follow a link in the app, or press the browser's back button. Choose leave and drop edits to go on, or cancel to stay and save.
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.
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.
GET /api/messages/stream is an SSE feed of the messages between agents, and
from senders that are no agent (the operator, reminder, compact) to an
agent. It reads the terminal rows on $SWARM.term.<agent> and has no hive in
the path. Each event's data is {ts, from, to, body, body_format?}. An
agent's send row gives one event from that agent to the recipient in the
row's summary. A turn started by a non-agent sender gives one event to the
agent whose terminal holds the row. A turn started by an agent gives none,
since that agent's send row already did. The query since_minutes (default
60, at most 1440, 0 for live only) sends recent history first, oldest first,
read from each agent's newest 100 rows. A message an agent drained with an
in-turn recv starts no turn and so is missing.
The messages page (/messages) reads that feed. A history select picks
the window, from live only up to 24 hours, and a filter keeps the messages whose
sender or recipient contains the text you type. Each message is one row of the
same list an agent's terminal uses, a summary line with the time, the two
parties and the start of the body, and the whole body under it. The list
follows the newest message unless you scroll up, and holds the last 2000. The
feed has no event id, so a reconnect sends the history again, and the page
empties its list each time the connection opens, then fills it from the replay.
Changing the window opens a new stream.
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.
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. Like the agent's
terminal route below, it creates a short-lived consumer on that stream, here
filtered to the one subagent, which sends its newest 100 rows and then each new
one, with the same Last-Event-ID resume. The stream holds every subagent of
the agent, so for a fresh reader the consumer starts at the oldest row and
swarm-controller drops all but the newest 100 before sending. 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.
Issue report
The issues page lists the open issues of every repo that has one, from
GET /api/issue-report, or of one repo from
GET /api/repos/{org}/{repo}/issue-report. Each row shows whether an open
dependency blocks the issue and how many other issues depend on it. The
list sorts by that count by default, and hide blocked drops the blocked
rows.
Picking a repo fetches its report again. The page remembers the pick, so a repo that no longer has an open issue stays in the repo select with the label no open issues, and the table stays filtered to it. The repo list and the report fail on their own: a repo list that fails to load shows its error beside the report, and a report that fails to load clears the table instead of leaving the previous repo's issues under the error.
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, switching to all if the filter hid any, 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 describes the settings themselves.
Editing an agent's instructions
The instructions row of an agent's detail panel has an edit button. It
opens AGENTS.md, the file the agent reads its instructions from, in a plain
text box. The text is the copy the agent last published, so the box still
opens for a stopped agent, but save needs the agent running.
Save sends the swarm a diff against the version you opened, and the agent applies it to its own file with git. When the agent changed the file in the meantime, the swarm refuses the diff. The box then keeps your text, shows the agent's version below it, and offers two ways on: keep my text, replace the agent's changes, which makes your text the new base for the next save, or use the agent's version, which drops yours. A diff that git refuses for any other reason shows git's message and leaves the file as it was. Closing the dialog keeps an unsaved edit for the next time you open it.
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; when that read fails, the panel shows the
reason and a retry button. hivectl
sets the hive's own limits.
Permissions
The agent's detail panel shows what the swarm grants it under permissions:
the default set, none, or a count with one badge per permission. A swarm
grant is the agent's whole set, so it follows the agent to whichever hive runs
it. edit opens a dialog with one checkbox per permission the controller
knows (GET /api/permissions), ticked as the grant stands, or on the default
set when the swarm sets none.
Saving does a PUT /api/agents/{agent}/permissions of the ticked boxes. A
PUT replaces the stored list whole, so an unticked box revokes that
permission, and ticking nothing grants nothing. Ticking exactly the default set sends the
DELETE instead, as the create form sends no list for it, so the agent follows
the default if it changes. use the default sends the DELETE too. The
dialog shows a refusal's reason under the boxes and leaves them as you ticked
them.
A saved change isn't instant. The hive reads the grant about every 30 seconds
and the agent takes it at its next turn. A change to manage_root_agent
restarts the agent after its current turn. The panel reads
GET /api/agents/{agent}/permissions; when that read fails, the panel shows
the reason and a retry button. swarmctl agent permissions set sets the
same grant from a shell.
Creating an agent
The + agent button opens a dialog of five steps, which you go through in order with next and can step back through with back or the step tabs:
- identity: the name, the hive, and optionally an archetype that fills in the settings of the steps after it. next waits for a valid name (1-63 lowercase letters, digits and hyphens) and a hive.
- instructions: the text the agent's
AGENTS.mdstarts with, in a box of its own. - permissions: one checkbox per permission, starting from the default set or the archetype's.
- resources and inference: the optional resource caps and inference settings.
- summary: every setting the request will carry, and what stays at its default, then what this creates. create is on this step only.
The header, the step tabs and the buttons stay in place and only the step's own
fields scroll, so a long step never pushes next out of view. After
create, the dialog says it queued the job and links to jobs, and offers
create another and close. create another starts on step 1 with every field back at
its start, and keeps only the hive you picked. If the controller refuses the AGENTS.md text,
the dialog goes back to step 2 and shows the reason there.
Settings at creation
Step 4 of the + agent dialog has 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. 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.
The dialog loads the hive list and the permission list when it opens. If either
load fails, the dialog shows the reason in an error panel above the step's fields, and
the hive picker or the permissions area reads could not load hives or
could not load permissions in place of loading….
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.
Initial AGENTS.md at creation
Step 2, instructions, holds a plain text box for the
text the new agent's AGENTS.md starts with. Left empty, or holding only
whitespace, it sends nothing and the agent starts without the file. Otherwise
the text goes as typed in the optional agents_md field of POST /api/agents.
The agent writes it on its first start, and only when it has no AGENTS.md
yet, so creating an agent again under a name that has one never replaces it.
The controller refuses a text over 64 KiB. Its reason shows under the
box, and the swarm creates no agent. After a successful
create the box empties. The
instructions editor changes the file later.
The swarm README has the wire details.
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. When the panel
fails to read the accounts, the inference settings, the resource caps or the
permissions, it
shows the reason as text. A list the panel already loaded stays on screen
beneath the error.
| 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 route names the second kind forgejo; the panel shows it as forge,
the word the link menu uses.
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 atswarm/agents/<agent>/forge/<label>. The agent'shive-agent-forge-accountsunit fetches it into<state>/forge-<label>-tokenand<state>/forge-<label>.json, the fileshive-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-accountwith a personal access token, stored atswarm/agents/<agent>/github-token. One token per agent. The agent'shive-agent-github-tokenunit fetches it into<state>/github-token, the file itsghwrapper, git credential helper and GitHub notification poller read. Agithub-tokenalready 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-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 oneserver_namedon'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.
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.
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.
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
sso.md— the authelia instance itself, and the user store.../networking/gateway.md— the full vhost map and TLS modes.