diff --git a/docs/2-handbook/external-accounts/forgejo.md b/docs/2-handbook/external-accounts/forgejo.md index 0e510430d..3a3aa75ee 100644 --- a/docs/2-handbook/external-accounts/forgejo.md +++ b/docs/2-handbook/external-accounts/forgejo.md @@ -14,7 +14,7 @@ Two labels can name two forges. In the swarm UI, open an agent, choose **link account**, then **forge account**. Fill in a label, the forge's URL and an access token. The form shows the controller's answer if it refuses the input, and the linked account appears -in the agent's list of [linked accounts](../swarm/ui.md#linking-external-accounts) +in the agent's list of [linked accounts](../../4-implementation/web-ui/swarm-ui.md#linking-external-accounts) as kind **forge**. - `label` is lowercase letters, digits and hyphens. It names the account in the diff --git a/docs/2-handbook/external-accounts/github.md b/docs/2-handbook/external-accounts/github.md index cb5a034e6..4a9d39475 100644 --- a/docs/2-handbook/external-accounts/github.md +++ b/docs/2-handbook/external-accounts/github.md @@ -36,7 +36,7 @@ to `/github-token` at runtime (see [Provisioning](#provisioning)). ## Provisioning -The PAT is operator-supplied. In the [swarm UI](../swarm/ui.md#linking-external-accounts), +The PAT is operator-supplied. In the [swarm UI](../../4-implementation/web-ui/swarm-ui.md#linking-external-accounts), open the agent on `/agents`, choose **link github account** and paste the PAT (`PUT /api/hives/{hive}/agents/{agent}/github-account`). swarm-controller stores it at `swarm/agents//github-token` in the swarm secret store; diff --git a/docs/2-handbook/external-accounts/gitlab.md b/docs/2-handbook/external-accounts/gitlab.md index 58222a4f4..84b311ae6 100644 --- a/docs/2-handbook/external-accounts/gitlab.md +++ b/docs/2-handbook/external-accounts/gitlab.md @@ -15,7 +15,7 @@ In the swarm UI, open an agent, choose **link account**, then **gitlab account**. Fill in a label, the instance URL (it starts as `https://gitlab.com`) and the token. The form shows the recommended scopes and the controller's answer if it refuses the input. The linked account appears in the agent's list -of [linked accounts](../swarm/ui.md#linking-external-accounts) with a delete +of [linked accounts](../../4-implementation/web-ui/swarm-ui.md#linking-external-accounts) with a delete action. The UI calls the controller's API, which you can also call directly: ```text diff --git a/docs/2-handbook/external-accounts/matrix.md b/docs/2-handbook/external-accounts/matrix.md index 77fa76c5d..c7977c17c 100644 --- a/docs/2-handbook/external-accounts/matrix.md +++ b/docs/2-handbook/external-accounts/matrix.md @@ -24,7 +24,7 @@ account**. You can give the account in two ways: encrypted rooms. Prefer the password login. The linked account appears in the agent's list of -[linked accounts](../swarm/ui.md#linking-external-accounts) as kind +[linked accounts](../../4-implementation/web-ui/swarm-ui.md#linking-external-accounts) as kind **matrix**. The controller refuses a name that already holds an account; delete it and link again to replace it. Linking also allows the homeserver's host in the agent's outgoing internet access, and deleting the account removes it again. diff --git a/docs/2-handbook/hives/README.md b/docs/2-handbook/hives/README.md index 9d14506e0..dd191aab0 100644 --- a/docs/2-handbook/hives/README.md +++ b/docs/2-handbook/hives/README.md @@ -75,7 +75,7 @@ VictoriaLogs; read them in Grafana's **Explore**, as **Set up a schedule.** Scheduled prompts are swarm-level: set them with `swarmctl schedule` ([`swarm/schedules.md`](../swarm/schedules.md)) or in the -schedules card on the swarm UI's messages tab ([`swarm/ui.md`](../swarm/ui.md)). +schedules card on the swarm UI's messages tab ([`swarm-ui.md`](../../4-implementation/web-ui/swarm-ui.md)). ## More depth diff --git a/docs/2-handbook/swarm/ui.md b/docs/2-handbook/swarm/ui.md index 7c15d8d3a..798fba125 100644 --- a/docs/2-handbook/swarm/ui.md +++ b/docs/2-handbook/swarm/ui.md @@ -7,21 +7,8 @@ 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. +The swarm UI is a set of pages. Each one is a card list or a table you open in +place, and every one reads from [`swarm-controller`](../../../swarm-controller/README.md): | route | what | | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -34,941 +21,14 @@ smaller; short confirm dialogs stay short. | `/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. While a page is fetching, its refresh picker shows a spinner in place of -the clock, for at least 0.4 seconds so a quick answer still reads as a refresh. -On a folded card (jobs, builds, hives) the picker sits in the card's header, left of the -chevron, and shows once the card is open; selecting it doesn't fold the card. - -The pages that only display (agents, alerts, builds, review runs and the hives -card) show the answer they last saw at once, then replace it with the fresh one -while the spinner runs, so opening one never starts on an empty page. On the very -first visit, the hives card and the alerts table show their own header with three -grey placeholder rows until the first answer comes. In an opened agent card, a -setting still loading (permissions, resource caps, internet, allowed -domains) shows a grey pill the size of its badge, and the AGENTS.md editor a grey -block. The browser -keeps the answer for itself only, and forgets it after a day, after a new build -of the UI, at **log out**, and when the login ends. The pages with an editor -(schedules, notification flows, broadcast) start empty as before. - -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. When the secret store has the login, the dialog closes and a -short toast in the top-right corner says so. 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 settings section of an opened card has a **claude login** row: when -the swarm stored the agent's login, or that none is, with a **delete login** -action that asks first. +`up` from its card. A card shows **login needed** when the agent's claude has no +session, with a **log in** button that runs the claude login from a dialog (see +[`claude-login.md`](claude-login.md)). The **wanted** badge changes an agent's +declared state, destroy included. -### 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, the status message with how long ago the agent set it (`5m ago`), a **terminal** -button that opens the agent's terminal tab, the **wanted** menu (up, offline, -paused, destroy), a **paused by hive** badge when the agent's own harness paused -its turn loop (hover for the reason; set the agent **up** to resume it), 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. While the roster loads for the first time, the page shows grey -placeholder cards with the same layout as the real ones, so the cards don't jump -when the data arrives. The placeholders stand still where motion is off. - -An opened card has four sections, and each reads nothing until you open it. -**status** and **terminal** start open, the other two folded: - -- **status**: how fresh the agent's last report is (with its age), its 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 an - agent built from an archetype has no config repo, so it gets no config repo - link), 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//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//terminal`, or a subagent's) the -header has an **N running subagents** badge. It opens a list of the agent's -subagents. Choosing one puts its terminal to the right of this one in a -[group](#groups-of-terminals); in a group, it splits that pane. A subagent -already in the group shows a marker and does nothing when chosen, and **maximize** -opens a pane's terminal on its own. On a subagent's own 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/`. 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. A pane can hold a subagent's -terminal (its header reads `agent/subagent`), put there by the **subagents** -badge in the pane's header. Two icon buttons sit in a pane's top right corner. -**Maximize** (the arrows pointing to the corners) opens that pane's own terminal -in its tab, or goes to the tab when one is open already. **Close** (the cross) -takes the pane out of the group, and a group left with one pane 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](../../4-implementation/swarm/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](../../4-implementation/swarm/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. While the first -answer loads, the section shows three grey placeholder cards. - -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](../../4-implementation/swarm/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 a thousand rules, the page shows the reason and -leaves your classifications as they were. **add a classification** stops at the -**rules per review** limit of the classifier settings below, ten until set. - -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`](../../4-implementation/swarm/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](../../4-implementation/swarm/review-classifier.md#the-service-account)). - -#### Classifier settings - -The **classifier settings** section shows the runtime, model and -effort the [review classifier](../../4-implementation/swarm/review-classifier.md) runs with and how many -requested reviews run at once, and lets you change them. The **PR reviews list -only failed rules** checkbox makes the review the classifier posts on a PR leave -out the rules that passed, didn't apply or were below their threshold; try-outs always show every rule. 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 provider's fields sit under their own **provider** -heading, the URL and the model each on a row of their own and the prices in a -grid on the same columns as the fields above. 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. Rules per review is the most classifications -one run carries, a whole number from 1 to 1000, ten until you set it. The -classifications of a run go one after another, each for up to three minutes, so -a high limit means a long run. A repo whose rules file holds more than the -limit gets a no-verdict review, and a try-out with more gets a 400. - -**save** is on once something changed and the fields are valid. A save changes -the runtime, model, effort and limits, 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. -While the schedules load for the first time, the card shows grey placeholder -cards with the same layout as the real ones. - -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](../../4-implementation/swarm/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. While the flows and the recent deliveries load for -the first time, the card shows grey placeholders of the same shape. - -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](../../4-implementation/swarm/notify-router.md#deliveries-and-dry-runs). - -#### Editing a flow - -`/notifications/` 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=`, 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 are Jinja templates over the event's fields, such as `{{ repo }}#{{ number }}`; -- 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](../../4-implementation/swarm/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, with the agent's name as its placeholder. **send** hands the text to the agent's -harness, and the agent takes it as an inbox message from your authelia username. -While the request is pending, a spinner replaces the button's label. When it -ends, the box empties: 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. - -
How a message reaches the agent - -The gateway sets `Remote-User` from authelia's answer on every `/api/` -request, replacing any the browser sent. `POST /api/agents//input` -takes that header as the sender and sends `{from, body}` as a NATS request on -`$SWARM.input.`. 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.` 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//cancel` sends `"cancel"` as a NATS request on -`$SWARM.control.`, and only swarm-controller may publish there. The -harness answers `{acted}`, which says whether a turn was running when the -cancel stopped it. It runs the same code as the hive dashboard's cancel button. -Unlike input, the harness answers a command, so swarm-controller returns that -answer as 200, 503 for no subscriber, and 504 when the harness held the command -and didn't answer within ten seconds. A 504 means unknown, not cancelled. - -
- -
Where the subagent list and rows come from - -The agent's subagent daemon publishes each subagent's terminal rows on -`$SWARM.term..sub.` with the agent's own queue credential, -into a stream named `term-sub-` that swarm-controller creates for every -declared agent. The stream keeps rows for 24 hours. swarm-controller serves the list as -`GET /api/agents//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//subagents//term/stream`. Like the agent's -terminal route below, it creates a short-lived consumer on that stream, here -filtered to the one subagent, which sends its newest 100 rows and then each new -one, with the same `Last-Event-ID` resume. The stream holds every subagent of -the agent, so for a fresh reader the consumer starts at the oldest row and -swarm-controller drops all but the newest 100 before sending. An agent with no -store identity, or no queue address, publishes no subagent terminals. - -
- -
Where an agent's terminal history comes from - -The harness publishes the agent's terminal rows on `$SWARM.term.`, into -a stream named `term-main-` 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..sub`, so they never overlap. -`GET /api/agents//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. - -
- -### Issues - -The card starts shut. The old `/issues` address redirects to the reports tab, as `/alerts` does. - -The **issues** 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. - -### Pull requests - -The **pull requests** card, below the issues card on the **reports** tab, lists the open -pull requests of every repo that has one, from `GET /api/pr-report`, or of one -repo from `GET /api/repos/{org}/{repo}/pr-report`. It starts shut and reads -nothing until you open it. The **repo** select lists the repos from -`GET /api/pulls/repos`, and picking one fetches that repo's report. No filter -applies at first. - -Each row shows the repo, the PR, its title, author, labels, requested reviewers -and base branch, and what a reviewer or merger filters on: - -- **WIP**: the forge's draft flag. Giving the title a `WIP:` prefix sets it. -- **approvals**: how many approvals count against how many the base branch's protection - rule asks for (`1/2`). An approval counts when it stands as the reviewer's latest - decision, on the PR's current head, neither stale nor dismissed, and from a - reviewer whose approval the rule accepts. A reviewer whose latest review asks for changes shows **changes - requested**. Filter the column to **complete** for the PRs whose approvals meet - the rule, or **missing** for the rest. A PR into a branch with no protection - rule has nothing to approve, so it counts as complete. -- **diff**: the lines the PR adds and removes (`+120 −8`), as the forge counts them. -- **conflicts**: the forge can't merge the PR as it stands. -- **protected base**: the base branch has a protection rule. A stacked PR targets a - feature branch, so filtering this to **no** lists them. -- **CI**: the commit statuses on the head, as one state (`success`, `pending`, - `failure`, or a dash when it has none). - -Every column sorts, and the columns with a small set of values (repo, author, -labels, reviewers, base, the flags and CI) filter by picking values. The browser -keeps the filters you set and the repo you picked. The card fetches the report once per -repo choice, with no refresh timer. - -### Builds - -The builds sit in a card under the job graph on the **jobs** tab. While the graph -loads for the first time, it shows grey placeholder rows in the shape of a job tree. 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. While the first answer loads, the -card shows three grey placeholder cards. - -The **Details** link on a `swarm-builder` commit status opens the jobs tab as -`/jobs?run=`, with the builds card open and that build's card in view. Links made before the -builds moved, to `/builds?run=`, 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. -While the agents load, the grid shows grey placeholder chips of the same size. -**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](../../4-implementation/swarm/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](../../4-implementation/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](../hives/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. The box lists every permission at once, without a -scroll area. - -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). While a list loads, it shows grey placeholder -rows. **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": "agent1"}` (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](../../4-implementation/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 controller page](../../4-implementation/swarm/controller.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//matrix/` | the account | its `homeserver`, if set | -| forgejo | `swarm/agents//forge/