1064 lines
65 KiB
Markdown
1064 lines
65 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 press, 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 press take
|
||
the highlighted one. **Escape**, **Tab** or a press 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.
|
||
|
||
A dialog grows to fit its content, up to 64em or nine tenths of the window, whichever is
|
||
smaller; short confirm dialogs stay short.
|
||
|
||
| route | what |
|
||
| ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `/` | the start page: the swarm's name over a digital-rain backdrop with the slug (`hyperhive / we are the wired`, its shaded gradients pulsing in a wave unless reduced motion is on) at the foot, plus a paperclip offering to set up an agent while no `ruth` exists, or her hive declares her destroyed (it opens the create form with `ruth` filled in); the swarm name in the header links here |
|
||
| `/agents` | every agent as a card that opens in place (status, terminal, subagents, and settings with permissions, internet, allowed domains and accounts); create agents (with their permissions, optionally from an [archetype](archetypes.md)), link forge, matrix and GitHub accounts |
|
||
| `/swarm` | what belongs to the whole swarm: a folded **hives** card with the hive directory and each hive's last reported status, a folded **egress** card with the swarm-wide allowed and denied domains, and a folded, **beta** **notification router** card (with a warning banner) holding the router's flows: a switch for each built-in, every flow drawn as a graph, and an editor for custom flows. `/notifications` redirects here |
|
||
| `/agents/<name>/terminal` | one agent's live terminal |
|
||
| `/agents/<name>/subagents/<subagent>/terminal` | one of that agent's subagents' live terminal |
|
||
| `/reports` | two folded cards, each read only once opened: 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, and a cross-repo **issue report** |
|
||
| `/jobs` | the controller's job graph, where agent creation and credential mints show progress, and under it the swarm builder's recent builds (agent config PRs, and agents built from `internal/meta` before a deploy), with what nix printed when one failed, and the review classifier's recent runs, a try-out on a real PR, and its settings |
|
||
| `/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 more than one agent at once, and a folded **schedules** card holds the scheduled prompts: add, edit, pause, resume and remove |
|
||
|
||
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.
|
||
|
||
### The agents page
|
||
|
||
`/agents` is a column of separate cards: a filter card, then one
|
||
card per agent, none of them inside a bigger card. A card's
|
||
header shows its icon, name, freshness, the status message, a **terminal**
|
||
button that opens the agent's terminal tab, the **wanted** menu (up, offline,
|
||
paused, destroy), and a **login needed** badge with a
|
||
**log in** button when its claude has no session. Select the header to open the
|
||
card in place; select it again to close it. Several cards can be open at once,
|
||
and a card stays open across the page's refresh, but not across a reload. The
|
||
filter card, which also holds the **+ agent** button and the refresh picker,
|
||
filters by name, hive, status, message, wanted state
|
||
and config PR.
|
||
|
||
An opened card has four sections, and each reads nothing until you open it.
|
||
**status** and **terminal** start open, the other two folded:
|
||
|
||
- **status**: the agent's turn state, context and cost, its hive, links to its
|
||
profile and its config repo on the swarm's forge (built from the forge entry
|
||
of `GET /api/links`; the row stays hidden when the swarm lists none), and its
|
||
open config PR.
|
||
- **terminal**: the live terminal with its message box.
|
||
- **subagents**: the agent's subagents and their terminal preview (next section).
|
||
- **settings**: inference, instructions, resource caps, internet, allowed
|
||
domains and permissions, then the agent's linked accounts and the menu that
|
||
links another. Internet, allowed domains and permissions show a badge; select
|
||
it to open the control that changes it (described below).
|
||
|
||
### Agent and subagent terminals
|
||
|
||
An opened agent card on `/agents` shows, in its **terminal** section, 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.
|
||
Each subagent in the list shows the model and effort it runs, like
|
||
`opus · high`, beside its name; one on the default model shows only the effort.
|
||
The `/agents/<name>/terminal` page's subagent list shows them too.
|
||
|
||
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
|
||
shows the latest report the controller holds, if it's under 150 seconds old, as
|
||
soon as you look. It reads "no report yet" when there is none: for an agent
|
||
that's offline, and until the first report after the controller restarts. 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 and the effort, beside the
|
||
connection badge; the turn-state badge says how long the agent has been in that
|
||
state.
|
||
|
||
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 developer details** 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. It also hides the **components** tab, a page for developers, which
|
||
still answers at `/components`. The browser keeps the choice for the swarm UI, apart from the
|
||
per-agent pages'.
|
||
|
||
The menu also has a **solid backgrounds** toggle. It turns the frosted, see-through
|
||
surfaces opaque: the header bar, the badges and the terminal panes. It stays off
|
||
until you turn it on, and the browser keeps the choice.
|
||
|
||
The menu also has an **expand tool output** toggle. It opens tool-output rows
|
||
expanded; rows already on screen keep their state, and rows that arrive after
|
||
you flip it follow it.
|
||
|
||
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 and folded **status** and **settings** sections (inference, resource caps, internet, allowed domains, permissions and linked accounts) as the
|
||
opened card on `/agents`, without the terminal and subagents. 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.
|
||
|
||
#### Groups of terminals
|
||
|
||
Terminals can sit side by side in a group, split the way tmux splits panes. Drag
|
||
another agent's terminal tab from the header onto the left, right, top or
|
||
bottom half of a terminal: the half it would take lights up, and the drop makes
|
||
a group of the two, with its own header tab at `/agents/group/<id>`. In a group,
|
||
the same drag onto a pane splits that pane, as many times as you like, and
|
||
dragging a pane's header badge onto another pane moves it there. The **split**
|
||
button in a pane's header does the same for an agent whose terminal tab is open:
|
||
**right** or **below** puts it beside the pane. The **×** takes the pane's
|
||
agent out of the group, and a group left with one agent goes, landing on that
|
||
agent's own terminal. Drag a divider to resize, or focus it and use the arrow
|
||
keys. Panes have no minimum size; a divider stops at 2% so a pane can't
|
||
vanish.
|
||
|
||
A group's tab shows its agents, `foo + bar`. Double-click it to give it a
|
||
name; clear the name to go back to the agents. Closing the tab closes the
|
||
group. An agent in a group still has its own terminal tab, and you can open it
|
||
full screen a second time. Groups and their layouts live in this browser
|
||
only, like the open terminal tabs, so they survive a reload but aren't shared
|
||
with another browser.
|
||
|
||
### Review
|
||
|
||
The **review** card on the **jobs** tab is the one place for the
|
||
[review classifier](review-classifier.md), with three sections in a column. The
|
||
card starts shut and reads nothing until you open it; the old `/review` address
|
||
redirects to the jobs tab. **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 is open with the card. 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
|
||
|
||
The **schedules** card on the **messages** tab holds one card per scheduled
|
||
prompt. It starts shut and reads nothing until you open it; the old `/schedules` address
|
||
redirects to the messages tab. 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
|
||
|
||
The **notification router** card on `/swarm` (beta, folded; `/notifications` redirects to it) 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.
|
||
While the agent is in a turn, a **cancel turn** badge appears among the state
|
||
badges over the terminal, in its own tab. It asks the agent's harness to stop
|
||
the turn, then shows whether one was running ("turn cancelled" or "nothing was
|
||
running"); a failure shows "cancel failed" with the reason on hover, and a click
|
||
tries again. Subagents have no message box and no cancel badge.
|
||
|
||
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. Each row also carries
|
||
the `Swarm-Model` and `Swarm-Effort` message headers, the model and effort the
|
||
subagent runs with, and the list returns them from the newest row as
|
||
`model` and `effort`. A subagent on the default model has no `Swarm-Model`
|
||
header and lists `model: null`; every row has the headers, so a subagent that
|
||
runs past the 24-hour window still shows them. 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 card starts shut. The old `/issues` address redirects to the reports tab, as `/alerts` does.
|
||
|
||
The **issue report** card on the **reports** tab 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 sit in a card under the job graph on the **jobs** tab. Both cards start
|
||
shut, and each reads nothing until you open it; a link to
|
||
a build opens the builds card. The old `/builds` address redirects there and
|
||
keeps its query.
|
||
|
||
The **builds** card lists what the swarm builder built, newest first, from
|
||
`GET /api/builder/runs`: agent config PR heads, and the agents it builds from
|
||
`internal/meta` before each deploy (a version bump, the pin fan-out or a merged
|
||
config PR). Each card shows the repo and PR, or for a deploy's build the agent
|
||
and `internal/meta`, 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 the jobs tab as
|
||
`/jobs?run=<id>`, with the builds card open and that build's card in view. Links made before the
|
||
builds moved, to `/builds?run=<id>`, redirect to the same place. 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 more than one agent 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
|
||
|
||
An opened agent card shows, in its **settings** section, 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 card 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 opened agent card's **settings** 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
|
||
|
||
An opened agent card shows, under **settings**, 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.
|
||
|
||
Below the list, **internet** shows whether the agent's traffic may leave the
|
||
host unfiltered: `unrestricted`, `restricted`, `log`, or `unrestricted (legacy)`
|
||
when the swarm never set the flag, which counts as on. `log` is `restricted`
|
||
that allows what the list would refuse and logs each such request as
|
||
`would_deny`; it stays until you change it. The badges for the other two
|
||
modes (**allow unrestricted**, **log**, **restrict**) set the mode with a
|
||
`PUT` that carries the stored permission list along with the flags, because a
|
||
`PUT` without the list resets a stored list to the default. An agent with no
|
||
stored list sends the flags alone. `log` sends `unrestricted_internet: false`
|
||
with `internet_log: true`; the other two leave `internet_log` out, which ends
|
||
logging. The **would-deny** tab of the **egress** dashboard in Grafana shows the hosts a
|
||
`log` agent reached.
|
||
The hive's egress proxy applies the flag to each agent's proxy credential (see
|
||
[the egress proxy](../networking/network.md#egress-proxy-squid)). An agent sends its
|
||
credential from its next start after it has fetched it, so a flag change applies
|
||
to an agent's traffic from then on.
|
||
|
||
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 card reads
|
||
`GET /api/agents/{agent}/limits`; when that read fails, the card shows the
|
||
reason and a **retry** button. [hivectl](../tools/hivectl.md#per-agent-resource-limits)
|
||
sets the hive's own limits.
|
||
|
||
### Permissions
|
||
|
||
An opened agent card shows, under **settings**, what the swarm grants it under **permissions**:
|
||
a badge reading the default set, none, or a count. A swarm
|
||
grant is the agent's whole set, so it follows the agent to whichever hive runs
|
||
it. Selecting the badge opens a box under it 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.
|
||
|
||
Each tick saves at once, as 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
|
||
box shows a refusal's reason under the boxes and puts them back as stored.
|
||
|
||
The agent's **internet** access is its own row beside it: a badge reading
|
||
restricted, log or unrestricted. Selecting it lists the three modes; picking one does a
|
||
`PUT` of the mode with the stored permissions beside it.
|
||
|
||
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 card reads
|
||
`GET /api/agents/{agent}/permissions`; when that read fails, the card shows
|
||
the reason and a **retry** button. `swarmctl agent permissions set` sets the
|
||
same grant from a shell.
|
||
|
||
### Allowed domains
|
||
|
||
An opened agent card lists, under **settings**, the destinations it may reach through its
|
||
hive's egress proxy under **allowed domains**, beyond the base list every
|
||
agent has: one badge per source with its count, **swarm-wide** (the entries for every
|
||
agent), **this agent** (the ones for it alone) and **automatic**. Selecting a badge
|
||
opens the names behind it. An entry matters
|
||
to a restricted agent. One with the unrestricted internet flag reaches any
|
||
name the proxy's private-range rules leave. **remove**, beside a name, drops one entry;
|
||
**add** opens a dialog with a domain box and a tick for every agent in the
|
||
swarm. The tick is off by default, so an entry is for this agent alone.
|
||
|
||
The **egress** card on `/swarm` has two lists of the swarm-wide entries, one per line, with
|
||
**remove** beside each and an **add** that opens the same dialog without the tick (the
|
||
entry is always for every agent). **allowed** is the list above. **denied** is the swarm's
|
||
deny list: every egress mode refuses a denied name, even when an allowed entry, a hive's
|
||
own `allowedDomains` or the unrestricted internet flag would let it through. The card says
|
||
which hives haven't applied the current deny list yet; until they do, their proxies may still let
|
||
those names through. The two lists use the same requests as the agent card, against
|
||
`/api/egress/allowed-domains` and `/api/egress/denied-domains`. The deny list has no
|
||
per-agent editor on the agent card yet, and no automatic entries.
|
||
|
||
An entry under **automatic** has no **remove**: the controller keeps
|
||
it from the host in the agent's stored API-key environment
|
||
(`ANTHROPIC_BASE_URL`, or any `*_BASE_URL` variable), so a restricted agent on
|
||
another provider reaches it. Saving or removing the environment changes it, and
|
||
the API refuses to add or remove one (`400` and `409`). An agent that only has
|
||
the hand-placed environment file gets none; add its provider's host here.
|
||
|
||
A domain is a DNS name of at least two labels, such as `github.com`. One
|
||
leading dot, as in `.githubusercontent.com`, also allows every subdomain; the
|
||
dot form is the only wildcard, because exact names can't list a service that
|
||
spreads over many hostnames, and an entry is the operator's choice. The
|
||
controller refuses IP addresses, other wildcards, ports and paths, and a name
|
||
the proxy already allows every agent (the base list). The dialog shows the
|
||
refusal's reason under the box and leaves what you typed. Adding does a
|
||
`POST /api/egress/allowed-domains` and removing a `DELETE` of the same path,
|
||
each with `{"domain": "github.com", "agent": "atlas"}` (leave out `agent` for
|
||
a swarm-wide entry); both answer the whole list, and `GET` reads it. Adding
|
||
an entry that exists changes nothing.
|
||
|
||
A saved change isn't instant. Each hive reads the list about every 30
|
||
seconds and reloads its proxy, so the name is reachable within a minute. The
|
||
proxy's private-range rules still apply: an entry whose name resolves to a
|
||
loopback, private or bridge address stays blocked. An entry adds to the names a
|
||
hive's own `allowedDomains` lists, which stay allowed. See [the egress
|
||
proxy](../networking/network.md#egress-proxy-squid) for what each hive does
|
||
when the controller is old or unreachable.
|
||
|
||
### 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, and an **Unrestricted internet** checkbox, unticked by
|
||
default. Ticking it adds `unrestricted_internet` to the request.
|
||
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 opened card's settings 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 four dialogs that write a credential for it
|
||
into the swarm secret store through swarm-controller. All four 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 fourth, the API-key environment, is the exception: a save overwrites what the
|
||
store holds.
|
||
|
||
An opened agent card lists its linked accounts at the end of its **settings** section, 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 card
|
||
fails to read the accounts, the inference settings, the resource caps or the
|
||
permissions, it
|
||
shows the reason as text. A list the card 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` |
|
||
| gitlab | `swarm/agents/<agent>/gitlab/<label>` | the label | its `url` |
|
||
| github | `swarm/agents/<agent>/github-token`, if any | `github` | `github.com` |
|
||
| `backend` | `swarm/agents/<agent>/backend-env`, if any | `backend-env` | none |
|
||
|
||
The route names the second kind `forgejo`; the card 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 card 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 gitlab account** — `PUT /api/hives/{hive}/agents/{agent}/gitlab-accounts/{label}`
|
||
with a GitLab base URL and an access token, stored at
|
||
`swarm/agents/<agent>/gitlab/<label>`. Link it from the agent's **link account** menu. What the token needs and how the agent uses
|
||
it: [GitLab accounts](../integrations/gitlab.md).
|
||
- **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).
|
||
- **set the API-key environment** — `PUT /api/hives/{hive}/agents/{agent}/backend-env`
|
||
with the text of a systemd `EnvironmentFile` (`NAME=VALUE` lines, at most
|
||
16 KiB), stored at `swarm/agents/<agent>/backend-env`. A save replaces the
|
||
stored text, including text the agent wrote into an empty leaf. The box is
|
||
write-only and starts empty; **remove** sends the `DELETE` below. A
|
||
swarm-controller without the route answers 404, and the dialog says so.
|
||
What the agent does with it:
|
||
[API-key provider](../turn-loop/config.md#api-key-provider-useapikey--backendenvironmentfile).
|
||
|
||
#### 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}` |
|
||
| gitlab | `DELETE /api/hives/{hive}/agents/{agent}/gitlab-accounts/{label}` |
|
||
| github | `DELETE /api/hives/{hive}/agents/{agent}/github-account` |
|
||
| `backend` | `DELETE /api/hives/{hive}/agents/{agent}/backend-env` |
|
||
|
||
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 card 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-reference.md`](credentials-reference.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."
|
||
|
||
## When your login ends
|
||
|
||
When the session ends while a page is open, the page's next `/api/` request (a
|
||
poll, a form, or a stream that dies) gets a plain 401 with a
|
||
`WWW-Authenticate` header from the gateway, and the page reloads. The reload
|
||
lands on the login and, after you log in, back on the same address. A page
|
||
request still gets the login redirect, and `swarmctl --swarm` still treats the
|
||
401 as a sign to log in again. The page skips a second reload within ten
|
||
seconds, so a page that keeps answering 401 can't loop.
|
||
|
||
## What the browser keeps
|
||
|
||
The vhost tells the browser what to cache. The bundle under `/static/` has
|
||
content hashes in its names, so the browser keeps it for a year. The entry
|
||
document, `/sw.js`, the manifest, the icons and the theme's `colors.css` have
|
||
fixed names, so the browser revalidates them on every load, and a deploy
|
||
reaches an open browser at its next load. A file that doesn't exist answers
|
||
404, including a `/static/` file from an older build, rather than the entry
|
||
document.
|
||
|
||
The service worker fetches its shell past the HTTP cache and refreshes the
|
||
cached entry document on each page load that reaches the network. When its
|
||
install fails, the previous worker keeps running and the browser console shows
|
||
why.
|
||
|
||
The pages carry a content security policy: scripts and connections only from
|
||
this origin, images from this origin, `data:` and https, and no framing from
|
||
another origin.
|
||
|
||
## 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.deploy.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 [`credentials.md`](credentials.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.
|