SelectField and the refresh-interval picker are built on a new Listbox, a select-only combobox: a button that keeps focus and a listbox in the top layer, so it works the same in every current browser, is not clipped by a scrolling ancestor and shows above a modal dialog. Keys, type-ahead and placement are pure functions with tests. required becomes aria-required, as a button has no native form validation.
860 lines
52 KiB
Markdown
860 lines
52 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
|
|
|
|
The signed-in user's avatar sits at the right end of the header on every page.
|
|
It opens a menu with the user's name and links to authelia's settings and
|
|
sign-out pages; the hive dashboard and the per-agent page have the same menu.
|
|
|
|
Every dropdown (a form's select, the refresh interval, the history window) opens a
|
|
list in the swarm UI's own theme, not the browser's, and works the same in every
|
|
current browser. Open it with a click, or with the arrow keys, **Enter** or
|
|
**Space** while it has focus. Then the arrow keys, **Home**, **End**, **Page Up**
|
|
and **Page Down** move through the options, and **Enter**, **Space** or a click take
|
|
the highlighted one. **Escape**, **Tab** or a click elsewhere shut the list without
|
|
changing anything. Typing the start of an option's name picks it, also with the list
|
|
shut. A dropdown inside a dialog opens above the dialog and the dialog doesn't cut it off.
|
|
|
|
| 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](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: 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 |
|
|
| `/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; a folded **broadcast** card above the feed sends one message to several agents at once |
|
|
| `/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`](../../swarm-controller/README.md).
|
|
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.
|
|
|
|
The **wanted** badge changes the state of an agent the swarm has declared. For an
|
|
agent the swarm never declared on its hive, such as one only its hive knows
|
|
about, the badge reads **no declaration** and doesn't open: create the agent
|
|
with `swarmctl agent create` or **+ agent**, which declares it `paused`. The API
|
|
answers `PUT /api/hives/{hive}/agents/{agent}/state` for such an agent with
|
|
404 and says to create it at swarm level. Destroying such an agent needs the same
|
|
create first, since the badge is the only way to destroy.
|
|
|
|
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. A subagent that drops out of the
|
|
list closes its terminal, and a later subagent with the same name starts
|
|
unpicked.
|
|
|
|
The preview carries only the connection badge and **expand**, so it stays clear
|
|
of the text. What the agent is doing shows in the details table above it, in
|
|
the **state** row: the turn state (idle, thinking or compacting), a **paused**
|
|
badge for a paused agent, and the tokens in its context and billed. The row
|
|
reads "no report yet" until the first report arrives, and for an agent that's
|
|
offline. The agent sends a report when its state changes and once a minute. If
|
|
the browser gives up on the stream, a **reconnect** badge opens it again. The
|
|
terminal's own tab keeps these badges, with the model, beside the connection badge.
|
|
|
|
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, state, 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](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.
|
|
|
|
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](notify-router.md#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](notify-router.md#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](notify-router.md)), 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.
|
|
|
|
<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.
|
|
|
|
`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.
|
|
|
|
</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`. 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.
|
|
|
|
</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>
|
|
|
|
### 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 messages page (`/messages`) has a **broadcast** card above the message list. It
|
|
starts folded; select its header to open it. The card holds a composer that sends one message
|
|
to several agents at once, and the messages show up in the list below as they
|
|
go. Folding it again keeps what you typed and ticked. 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 composer 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 composer 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](inference.md) 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](../tools/hivectl.md#per-agent-resource-limits)
|
|
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:
|
|
|
|
1. **identity**: the name, the hive, and optionally an
|
|
[archetype](archetypes.md) 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.
|
|
2. **instructions**: the text the agent's `AGENTS.md` starts with, in a box
|
|
of its own.
|
|
3. **permissions**: one checkbox per permission, starting from the default set
|
|
or the archetype's.
|
|
4. **resources and inference**: the optional resource caps and inference
|
|
settings.
|
|
5. **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](#editing-an-agents-instructions) changes the file later.
|
|
The [swarm README](README.md#an-agents-agentsmd) 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 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,
|
|
pinned-hive 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.
|