Watch
0
0
Fork
You've already forked hyperhive
0
Commit graph hyperhive/docs/swarm
Author SHA1 Message Date
atlas
993f5fd99d grafana: mint the admin password into the store and apply it on every start
Grafana's built-in admin kept the nixpkgs default password `admin`, and
Grafana's API takes basic auth whatever `auth.disable_login_form` says, so
the public vhost gave server-admin to `admin:admin`.

`swarm-bao-grafana-admin` creates a random password in the store under
Grafana's existing leaf when none is stored (`create` without `update`, so
the first one is kept), then copies it into the container.
`security.admin_password` reads it for a new database, and a `grafana cli
admin reset-admin-password` in `grafana.service`'s `preStart` applies it to
an existing one on every start.

Closes #5138
2026-10-12 15:16:53 +02:00
iris
cf4c4cde87 swarm-ui: load a repo's rules into the review try-out
A button beside the repo field reads the repo's rules from its default branch into the classification rows, names the file and the commit it read, asks before replacing rows the operator wrote, and shows the route's error without touching the rows.
2026-10-12 15:01:23 +02:00
atlas
2b1573c2ba swarm-controller: read review rules at a literal nested path
forgejo-api escapes the whole file path of a raw-file request, so the
rules file went out as `raw/.hyperhive%2Freview-classifier.yml` and
reached the forge only if every proxy and router in between decoded the
`%2F`. The controller now sends that one request itself: `raw_file_url`
escapes each path segment on its own and joins them with `/`, and
`forge::Client` holds a second HTTP client with the same token for it.

A 404 from the default-branch lookup now means "no commit on the
default branch", like an empty repo, instead of falling through to the
route's "no repo" answer. Only the repo lookup's 404 still means the
repo doesn't exist.

Tests cover the URL shape, the reads against a loopback stub forge
that serves only the literal path, the branch 404, 503 with no forge
access, 503 at the forge deadline on a paused clock, and a bad
polarity value.

Part of #5096.
2026-10-12 14:54:59 +02:00
atlas
16c7c249ef swarm-controller: read a repo's review rules from its default branch
`GET /api/review-classifier/rules?repo=owner/name` reads
`.hyperhive/review-classifier.yml` or `.json` from the repo's default
branch with the controller's forge token, on every request, and answers
the classifications in the try-out POST's shape, plus the file, the
branch and the commit it was read at. The swarm UI's try-out page loads
them into its editor (#5134).

The file is one `classifications` list of the try-out's `Classification`.
A missing file, both files, a parse error, any unknown key, an empty list
or more than ten entries is a problem+json error, never a pass. The
checks the POST makes on its classifications move into one function both
paths call.

The doc names the file and its schema, replacing the placeholders, and
documents the route.

Part of #5096.
2026-10-12 14:54:59 +02:00
iris
c5b66dc5a0 swarm-ui: match the try-out page to the backend's limits
The repo check takes the endpoint's own character set and refuses . and .. parts. The running and expired messages say a run can last up to half an hour and that a controller restart drops the result.
2026-10-12 14:51:38 +02:00
iris
a4a4381cf6 swarm-ui: try-out rows keyed by position, distinct names, and accurate storage wording
Two classifications sharing a name made the result table's row key collide, so the form now refuses duplicate names and the table keys rows by position anyway. The page said it stores nothing in the browser, but the result table remembers its column filters like every table does; the header comment and the docs now say that. The row cap's comment says to keep it in step with the endpoint's own cap.
2026-10-12 14:51:38 +02:00
iris
b67517e41b swarm-ui: a review try-out page
A page to try a review classification against a real PR: repo, PR number and one or more classifications (name, description, polarity, threshold). Run posts to the controller's try-out endpoint and polls until the run is done or failed, saying it is running meanwhile, then shows the review's table, the overall verdict and the head commit. A row that could not run shows its error in place of a reason, and a failed run shows its error. The inputs stay so the run can be edited and repeated. Nothing is stored and nothing is posted to the forge. Route, nav entry and a section in docs/swarm/ui.md.
2026-10-12 14:51:38 +02:00
atlas
34bde7a5af nix: allSwarmServices turns on the review classifier
deploy.review-classifier.enable now defaults to allSwarmServices, as the
other shared services do, but only where the host has the classifier's
store identity (deploy.bao.reviewClassifierClient{Cert,Key}File). The
classifier module asserts that pair; glue-bao-tls.nix mints it only on the
host that runs the store, so a services host with the store placed
elsewhere would otherwise fail eval. An explicit value still wins.

The operator still writes the Anthropic key by hand; until then each run
fails with "no API key…" while the service runs.
2026-10-12 13:11:21 +02:00
atlas
6c9cb66dce docs: the review classifier's try-out service, its key and its subject
- review-classifier.md: claude runs spawn through hive-claude directly
  with no tools and no session (hive-runtime holds only TurnStop); the
  answer's fields, the 500-character reason cap and what a failure does;
  how the try-out page reaches the classifier; the settled parts of the
  open list (API key path, try-out subject, --tools "" on claude).
  The banner stays.
- services.md: a Review classifier section with the two operator steps.
- secrets.md, credentials.md: the classifier's leaf and the key row.
- network.md: the container's row. CLAUDE.md: the crate.
2026-10-12 13:11:21 +02:00
atlas
7e6a4c7727 agents: remove send(operator) and the operator inbox; alert on turn failures
Agents reach the operator over matrix now (#5115). A `send` to
`operator` is refused, by the broker and by the MCP allow-list check,
with an error that names the matrix MCP tools. The dashboard's Y3R C4LL
tab goes with it: the operator inbox, `GET /api/operator-inbox`,
`Broker::unread_for_recipient`, and the browser notifications that only
ever fired on operator-bound messages.

The harness no longer posts turn failures and plugin-install failures
to the operator inbox. Both are already logged at warn; a new Grafana
rule, `hh-agent-turn-failed`, fires per agent on the harness's
`turn failed` line.

Dashboard-to-agent messaging (op-send, the agent web UI's message box,
mark-all-read for agent inboxes) is unchanged.

Decisions per mara on #5115: q1 a, q2 a plus a grafana alert, q3 keep
dashboard-to-agent messaging and leave swarm-level code alone.
2026-10-12 08:50:06 +02:00
atlas
2df19e4899 swarmctl: log in with the operator's own SSO account and reach the controller through the swarm domain
`swarmctl --swarm https://<swarm> login` runs the OAuth 2.0
authorization-code flow with PKCE against the swarm's authelia as a new
public `swarmctl` client: a pushed authorization request, the browser login
and explicit consent, and the code delivered by form_post to a loopback
port. The access and refresh tokens are cached at mode 0600 under
$XDG_STATE_HOME/swarmctl/ and refreshed under a file lock, since authelia
rotates the refresh token and revokes the login when a used one is
presented again. With --swarm (or SWARMCTL_SWARM), the agent, schedule and
forge verbs send their requests to https://<swarm>/api/ with the bearer
token instead of over the controller's unix socket, which stays the default.

authelia registers the client on every authelia host with a swarm domain:
`kind = "native"`, public, no minted secret, scopes offline_access and
authelia.bearer.authz, audience https://<swarm>/api/ (matched as a URL
prefix), redirect http://127.0.0.1/callback (any loopback port). Every
field is one authelia 4.39.20's validator requires for a public client
holding the bearer scope.

The swarm apex's /api/ answers a refused caller that sent an
Authorization header with the 401 itself instead of the login redirect,
so the CLI can tell "log in again" apart from an answer; a browser still
gets the redirect. The bearer token is not forwarded to the controller.

Refs #4409 (mara: c94764, c94782).
2026-10-12 01:25:58 +02:00
iris
afa9d861ba docs: swarm UI popover wording without the input-specific verb 2026-10-12 00:19:33 +02:00
iris
f957e19d0d swarm-ui: show an agent's details in a popover, not a modal dialog
The details open under the agent badge without dimming or blocking the page; a click outside or Escape closes it. New ui/popover primitive (anchored, portal, no backdrop) with a sample on the components page. A confirm or link dialog opened from inside it stays modal, and the popover ignores clicks and Escape that belong to that dialog. AgentDetailDialog becomes AgentDetailPopover.
2026-10-12 00:19:33 +02:00
iris
103c842890 swarm-ui: open an agent's details in a dialog from its terminal tab
The terminal page gets an agent badge (icon and name) that opens the agent's detail panel in a dialog over the terminal, without the terminal preview. The panel body and the actions on an agent (declare a wanted state, link an account, with their confirm and form dialogs) move out of the agent roster page into AgentDetail and useAgentActions, so the roster and the dialog share them. AgentIcon moves to its own file for the same reason.
2026-10-12 00:19:33 +02:00
iris
9761ffa24a swarm-ui: a terminal tab takes its agent's icon as the browser-tab icon
The agent terminal page swaps the page's favicon for the agent's icon while it is mounted, so tabs of different agents tell apart. A subagent terminal shows its agent's icon. An agent with no icon, or an unreachable controller, keeps the hyperhive mark; leaving the page restores the page's own icon.
2026-10-12 00:18:10 +02:00
atlas
f08a9cadb6 swarm-controller: end a dead replay, clamp stale resume ids, test on a real server
A replay consumer now has a 5s idle heartbeat, and the row stream ends at
the first consumer error instead of skipping it. When the server drops the
consumer, as on a restart, two missed heartbeats end the SSE response and
the browser reconnects with Last-Event-ID. Before, the response stayed
open and delivered nothing.

A Last-Event-ID above the stream's newest sequence now replays as a fresh
reader. The server accepts a start past the end and delivers nothing
until the stream reaches it, so a stale id stalled the replay.

swarm-controller's stream tests run against a real nats-server on
loopback (`test_nats`, binary from HIVE_NATS_SERVER, exported by
checks.nix and devshell.nix). They cover: ensuring the stream twice, an
existing stream with other limits being opened as it is, the replay
arriving before live rows, a reconnect getting every later row exactly
once, and a dropped consumer ending the row stream.

The limits test now compares against the subagent stream's own config.
Two comments that described the agent's terminal route as live-only are
corrected.
2026-10-11 23:26:24 +02:00
atlas
ae9ca4331e swarm: keep an agent's terminal rows and replay them on attach
swarm-controller now creates `term-main-<agent>` for every live agent, in
the same one-minute pass and with the same config style as
`term-sub-<agent>` (#4897): subjects `$SWARM.term.<agent>` only, max_age
24h, max_bytes 64 MiB, discard Old, max_message_size from the queue's
max_payload. An existing stream is opened as it is. The agent keeps its
plain publish on `$SWARM.term.<agent>` and gets no JetStream subject.

`GET /api/agents/{name}/term/stream` creates an ephemeral push consumer on
that stream starting 100 rows back, so a swarm-ui terminal opens with
recent history and then follows new rows, in order, from one
subscription. Each SSE event carries the row's stream sequence as its id,
and a reconnect's Last-Event-ID resumes after it. Without the stream the
route falls back to the live core subscription it used before.

The stream is `term-main-<agent>`, not `term-<agent>`: an agent may be
named `sub-x`, whose `term-<agent>` would be agent `x`'s subagent stream.
Its one literal subject cannot match `$SWARM.term.<agent>.sub.>`, which
needs more tokens, so the two streams never overlap.

The reader gains `$JS.API.CONSUMER.CREATE.*.*.$SWARM.term.*`, the
extended create form whose last tokens are the consumer's filter. Checked
against nats-server 2.14.4, the trailing `*` also admits a literal `*` or
`>` filter, so a consumer under it reaches any terminal row, main or
subagent; those are rows the reader already subscribes to and reads with
`STREAM.MSG.GET.*`.

Closes #4826
2026-10-11 12:35:31 +02:00
atlas
c6717bccb0 agents: re-pull the agent's own bao mTLS leaf on a timer
swarm-controller re-issues an agent's store leaf at half its validity, but
the running container kept presenting the copy its hive delivered at
start, so a re-issued leaf reached the agent only on its next restart.

hive-agent-bao-identity now seeds a live copy of the pair in its own
runtime directory from the delivered credentials, and re-runs every 10
minutes: it logs in with the live leaf, reads its own
swarm/agents/<agent>/bao-mtls (a read render_agent already grants), and
when the leaf changed, logs in with the new pair and swaps it in only if
the store accepts it.

Every unit in the container that logs in to the store presents the live
pair: hive-agent (queue-identity.nix), hive-matrix-daemon,
hive-subagent-daemon, hive-agent-forge-token, hive-agent-forge-accounts
and hive-agent-github-token. They keep loading the server CA as a
credential. The long-running daemons are ordered after the refresher so
the live pair exists when they start.

It renews only while the held leaf is still valid: the login that reads
the new leaf is made with the current one.

Refs #5017
2026-10-11 10:00:43 +02:00
atlas
ed271e655f agents: remove the hive-level /effort control too
mara on #5034: "effort shares /models fate". The per-agent web UI no
longer sets effort: the `/effort` slash command, the effort picker,
`POST /api/effort`, `Bus::set_effort`, the `hyperhive-effort` writer,
`EFFORT_LEVELS`/`is_valid_effort` and the ACP effort-list payload are
gone, and `lib/modelEffort.ts` with them. The effort chip is
display-only and shows its source (`effort_source` in `/api/state`);
on ACP it stays hidden while the session offers no effort level.

The read side is unchanged: with no swarm value an agent runs an
existing `hyperhive-effort` file, else its agent.nix effort, else
medium. Existing files are not touched.

Refs #5034
2026-10-11 01:28:44 +02:00
atlas
594693f4c7 agents: remove the hive-level /model control; the swarm editor sets the model
The per-agent web UI no longer sets the model: the `/model` slash
command, the model picker, `POST /api/model`, `Bus::set_model` and the
`hyperhive-model` writer are gone. The model chip is display-only and
shows where the model comes from (`model_source` in `/api/state`). The
model is set on the swarm (swarm UI agents page, `swarmctl agent
inference set`).

The read side is unchanged: with no swarm value an agent runs its
agent.nix model, else an existing `hyperhive-model` file, else haiku.
Existing files are not touched.

With the picker gone, its model list goes too: `available_models` in
`/api/state`, the ACP model-list filter, and the harness's global
`HIVE_AVAILABLE_MODELS`. The subagent daemon keeps its own
`HIVE_AVAILABLE_MODELS` from mcp.nix, and the `availableModels`
assertion stays.

`/effort` (command, picker, route, file) is unchanged.

Refs #5034
2026-10-10 23:11:53 +02:00
atlas
46e9966e17 docs/review-classifier: cost comes from OTEL labels, not the swarm UI 2026-10-10 19:30:25 +02:00
atlas
00ce00f33d docs: narrow the review-classifier runtime refactor to the per-turn seam 2026-10-10 18:56:30 +02:00
atlas
dcd005623e docs: make review classifier page generic over swarms
Drop the swarm-specific reviewer agent name and the hyperhive-repo
approval count from the architecture page and its index line.
2026-10-10 18:54:02 +02:00
atlas
c0af8d1fd3 docs: review classifier architecture (planned)
Add docs/swarm/review-classifier.md, the high-level architecture of the
planned PR review classifier pipeline, written as the full target shape
under a "planned, not implemented yet" banner. Link it from docs/README.md
and the docs/swarm/README.md piece list.

The page reflects the epic description and the operator's answers in the
epic thread; points still open are listed in its "Still open" section.
The banner names the epic by title rather than by number because the
tracker-tag lint rejects issue references in tracked files.
2026-10-10 18:53:41 +02:00
iris
87aeed32a7 docs: swarm UI inference section in active voice 2026-10-10 18:51:55 +02:00
iris
0d04bd7295 swarm-ui: per-agent inference panel and editor on the agents page
The agent detail panel shows the model and effort an agent runs on with
where each comes from, plus the swarm's subagent model list. An edit
dialog sets the swarm value (PUT) and a clear action removes it (DELETE).
Reads the configured/effective view, and shows the stored values alone
until an agent has reported what it runs on.
2026-10-10 18:51:55 +02:00
atlas
7364ccdc98 swarm-controller: GET inference answers configured: null without a key
`GET /api/agents/{name}/inference` answered `"configured": {}` both for
an agent with no key in `agent-inference` and for a stored value with no
fields, so a reader could not tell "the swarm sets nothing" from "a
setting with every field unset". It now reads the key as
`Option<AgentInference>`: an absent or deleted key is `null`, a stored
`{}` stays `{}`. The shared `schedules::read` helper is unchanged.

`swarmctl agent inference get` prints "no swarm setting; the agent runs
on its own model and effort" for `null`; `set` merges into an empty
value in that case, as before.

The agent itself still treats no key and `{}` the same.

Refs #5034
2026-10-10 17:37:23 +02:00
atlas
47bd83054f swarm-controller: refuse an inference model name that starts with "-"
`check_model` accepted a value like `--dangerously-skip-permissions`,
and the stored model ends up in the agent's `claude` argv. A PUT whose
`model`, or any `subagent_models` entry (same validator), starts with
`-` is now a 400 with the same problem+json shape as the other model
checks. A dash inside a name (`claude-opus-4-8`) is still accepted.

Raised by argus on #5084.

Refs #5034
2026-10-10 17:37:23 +02:00
atlas
19a045a401 hive-agent: re-send the turn-state header once a minute
The header on `$SWARM.agent-state.<agent>` went out only when a bus event
changed it, and the core subject keeps nothing for a late subscriber. An
idle agent therefore said nothing to a reader that attached after its
last change: swarm-controller after a restart showed `effective: null`
for that agent until its next turn, which can be hours.

The publish loop now also wakes every 60 s and re-sends the current
header through the same `publish` path (clearing the dedup state first).
Transitions still go out as they happen. Same subject, same grant.

An agent whose queue connection is down now logs `swarm agent state:
publish skipped` once a minute even while idle, so the
`hh-swarm-agent-state-publish-skipped` alert also fires for idle
disconnected agents, not only busy ones.

Refs #5034
2026-10-10 17:37:23 +02:00
atlas
d723fac0be swarm: show each agent's effective model and effort, and where they came from
Each agent now reports the model and effort it runs on, and where each
comes from (`swarm`, `agent_nix`, `local_file` or `default`), in its
turn-state header on `$SWARM.agent-state.<agent>`: `effort`,
`model_source` and `effort_source` join `model`. The shared
`swarm_queue_client::agent_inference::{Effective, Source}` types spell
those fields for both ends.

The Bus keeps a source next to the model and effort. `Bus::new` takes it
from the local fallback chain (`harness_state::local_model` /
`local_effort`, which now return the source with the value), a `/model`
or `/effort` pick sets `local_file`, and the swarm poller sets `swarm`
or, on a clear, the local chain's source. A swarm value equal to the one
the agent already runs is now applied once, so the source moves to
`swarm` and the header is republished; before, `next()` skipped it as
unchanged.

swarm-controller subscribes to every agent's header with the reader
grant it already holds for the state relay, and keeps the latest
inference fields per agent in memory (`inference::Reported`).
`GET /api/agents/{name}/inference` now answers
`{configured, effective}`, `effective` being `null` until the agent's
first header with these fields since the controller started.
`swarmctl agent inference get` prints both; `set` reads `configured`.

Refs #5034
2026-10-10 17:37:23 +02:00
atlas
5658361096 docs: describe the forge's OIDC secret as fetched from bao, not copied or hand-placed
sso.md still described a host-side copy unit and an operator-placed file
for a forge elsewhere; secrets.md's client-secret row still named
sso.clientSecretFile as the operator's route. The clientSecretFile
example now shows the path swarm-bao-forge-oidc writes.

Refs #5015
2026-10-10 14:07:31 +02:00
atlas
f6ca5f1433 forge: fetch the OIDC client secret from bao instead of a host-side copy
The forge's login source read its OIDC client secret from a file that
`hive-forge-oidc-secret` copied out of authelia's container tree. That
unit only existed where authelia ran on the forge's host (`ssoLocal`),
so a forge anywhere else needed the file placed by hand.

The publisher on authelia's host now carries the forge's client secret
to `secret/swarm/services/<forge client id>/oidc/client`, like every
other swarm service's. On the forge's host `swarm-bao-forge-oidc`, a copy
of `swarm-bao-matrix-oidc`, reads it under the forge publisher's leaf
(the forge host's one store identity, whose policy gains read on that
one path) and writes the file `forgejo-sso-source` reads, re-running the
login source when the value changes. It retries on the shared
store-retry shape while the store is down or the secret is not yet
published.

`hive-forge-oidc-secret` is removed, `clientSecretFile` defaults to the
in-container path wherever the forge runs, and the `ssoLocal` gate on
the login source's resolver ordering goes with it: the login source
fetches the issuer's discovery document on every forge host.

Refs #5015
2026-10-10 14:07:31 +02:00
atlas
90e6b984eb hive-subagent-mcp: check subagent models against the swarm's subagent_models
A `start` or `continue` naming a `model` now reads this agent's key in the
`agent-inference` bucket and, when `subagent_models` is set, checks the
model against that list instead of HIVE_AVAILABLE_MODELS (agent.nix
`availableModels`). An unset field, an absent key or an absent bucket
keeps today's check. A read that fails or takes over 2 s falls back to
HIVE_AVAILABLE_MODELS with a warning, so a subagent start never waits on
the queue. A call without a `model` is not checked and does not read.

The read uses the agent's own queue credential through swarm_term's
connect, and the one direct-get subject the agent is granted in that
bucket. The refusal names the list and that it came from the swarm.

Refs #5034 (S5)
2026-10-10 13:58:47 +02:00
atlas
7c50568db1 hive telemetry collector: fetch its OIDC secret from the store under the hive's own leaf
The hive collector's `oauth2client` secret came from a `clientSecretFile`
defaulting to authelia's own minted file on the IdP's host, with a
`hive-otel-client-secret` oneshot that only waited up to 120s for that
file to exist; any other hive had to copy the file across by hand.

`swarm-bao-hive-otel-oidc` now logs in with the hive's leaf
(`deploy.bao.clientCertFile`), reads `swarm/hives/<hive>/oidc/client`
(written by the publisher, granted by the hive leaf's
`swarm/hives/<hive>/*` read), writes it root 0400 when it differs, and
restarts the collector when the file is newer than its last start.
Retries every 30s for 24h (`lib/store-retry.nix`), like the matrix and
forge fetches. `clientSecretFile` defaults to that file wherever the hive
leaf is set and stays as an override; an overridden file renders no fetch.

otelcol-contrib 0.151.0's oauth2client re-reads `client_secret_file` on
every token fetch, but the path is `LoadCredential`'s start-time copy, so a
changed secret still needs a restart. `lib/refresh-consumer.nix` gains
`refresh_unit` for a host unit; `refresh_consumer` now calls it with
`--machine`.

nats-authelia's "consumer of the host client-secret dir" case pointed at
the collector's old default; it now reads the publisher's path unit.

Refs #5015
2026-10-10 01:10:31 +02:00
atlas
c2c61b6146 hive status publishing: read the hive's OIDC secret from the store, retry until it is there
hive-c0re publishes this hive's status to the swarm queue as the OIDC
client `hive-<name>`. Its secret came from
`deploy.hive-controller.statusPublish.clientSecretFile`, which defaulted
to authelia's minted file only on a host running both authelia and the
queue. Every other hive had no secret and never published, so the
controller showed it `never_reported`.

Now `swarm-secret-publish`, beside authelia, also copies each hive's
`hive-<name>` secret to `swarm/hives/<hive>/oidc/client`. Its policy
already allows `swarm/hives/*`. hive-c0re reads that path under the
hive's own store leaf, which the existing `swarm/hives/<hive>/*` grant
covers, and holds the value in memory.

The shared queue client no longer remembers a failed first connect for
the life of the process. A failed attempt is retried with a backoff
from 5s, doubling to a cap of 5 minutes; the dashboard banner stays up
until an attempt succeeds. The two status publishers wait for the
client instead of giving up on the first `None`, so a secret published
after boot is picked up without a restart.

Removed: the `statusPublish.clientSecretFile` option (both spellings,
now removed options with a message), its same-host default, its
LoadCredential, the assertion tying it to the other two coordinates,
and `swarm_queue_client::QueueConfig::from_env`, whose only caller was
this read. Publishing is now on wherever the queue URL and token
endpoint are set and the hive holds a store leaf.

Refs #5015
2026-10-10 01:05:33 +02:00
atlas
916fdb35b8 swarm: per-agent inference settings (model, effort, subagent_models) in KV
swarm-controller keeps each agent's inference settings in a new
`agent-inference` KV bucket, one key per agent, and is its only writer
(`GET`/`PUT`/`DELETE /api/agents/{name}/inference`; `swarmctl agent
inference get|set|clear`). Every field is optional. Each agent reads its
own key every 10 s through a direct-get grant on that key alone, and a
set `model` or `effort` replaces the Bus value, so the next turn runs on
it and the badge follows.

The swarm value is applied without persisting it: `Bus::set_model` and
`set_effort` are split into a persisting setter and a non-persisting
`apply_*`, and the poller uses the latter. Otherwise the swarm value
would overwrite `hyperhive-model`/`hyperhive-effort`, and clearing it, or
a restart, would no longer find the agent's own value. An unset field
changes nothing; a field cleared after being applied goes back to the
local value (`harness_state::local_model`/`local_effort`, the same
resolution `Bus::new` uses), read again at the clear.

`subagent_models` is validated, stored and shown by `get`; no agent
applies it yet.

Refs #5034
2026-10-09 23:13:13 +02:00
atlas
9b711d5ffa hive-forge: mirror actions/upload-artifact alongside actions/checkout
With actionsFromSelf, Forgejo resolves bare `uses: actions/<name>` against
the forge itself, so every action a workflow names needs a mirror there.
Only actions/checkout was seeded, and coverage.yml's
`actions/upload-artifact@v3` has failed at job setup with "repository not
found" since the CI runner moved to swarm-ci-runner (e2da5509).

The single hardcoded checkout mirror becomes a list of action names both
share, mirrored from github.com/actions/* like checkout already was. v3
points at the same commit (ff15f03) on github.com and code.forgejo.org.

The controller's forge-objects pass creates any missing mirror on every
pass, and the changed SWARM_CONTROLLER_FORGE_MIRRORS env restarts the unit
on switch, so an existing forge picks the mirror up on deploy.

Closes #5040
2026-10-09 10:40:28 +02:00
atlas
2d2d7529fc swarm-bao-forge-publisher re-runs on container start and token writes
The publisher was a oneshot with RemainAfterExit=true, started only through
`wantedBy = container@hive-forge.service`. Once it had run it stayed
`active (exited)`, so a later container start made its start a no-op: a token
re-minted without a host reboot or a unit change never reached the store.

- RemainAfterExit is dropped, so each start runs the script again.
- systemd.paths.swarm-bao-forge-publisher watches the token file in the
  container (PathChanged) and starts the publisher when the mint writes it,
  the same shape as swarm-secret-publish's path unit.
- The code comment, secrets.md and credentials.md now name both triggers;
  forge-placement pins the missing RemainAfterExit and the path unit.

Refs #4998
2026-10-08 13:45:56 +02:00
atlas
c45f7a5330 swarm-controller gains forge access when the token arrives, without a restart
A controller that started before the forge's host had published its token
ran without forge access until someone restarted it: on a split-host first
deploy that is a manual step. Now the forge client lives in a slot that is
filled once (`Late<T>`, an `Arc<OnceLock<T>>`):

- `forge::enable` tries the store once at start. On failure it logs once and
  re-reads in the background with backoff (30s doubling to a 5-minute cap,
  no log per retry), fills the slot and logs once when the token is there.
- The forge passes (#5014's per-hive token mint among them), the config-PR
  poll and the webhook registration start from that moment, through
  `start_forge`'s callback.
- Job nodes and HTTP handlers read the slot when they run, so they answer
  "no forge" before and work after.

A token replaced in the store after the slot is filled still reaches the
controller with its next restart. The round-1 change that shared
`queue_identity::fetch_with_retry` is reverted; nothing else uses it.

Refs #4998
2026-10-08 13:40:24 +02:00
atlas
121c4678aa swarm-controller reads its forge token from the store, not a host file
The controller's forge token never entered bao: the forge minted it in its
container, hive-forge-swarm-controller-token copied it to a host file, and the
controller LoadCredential'ed that file. forgeTokenFile defaulted to that path
only where the forge ran, so a controller on any other host had no forge
access unless an operator copied the file by hand.

Now the forge's host publishes it and the controller reads it:

- swarm-bao-forge-publisher (replaces hive-forge-swarm-controller-token)
  waits for the in-container token, logs in with its own forge-publisher leaf
  and writes it to swarm/controller/swarm-controller/forge/token. It runs on
  every forge container start, the old unit's trigger.
- New principal swarm-forge-publisher: leaf minted by glue-bao-tls.nix,
  policy create/update on that one path, written by
  swarm-bao-forge-publisher-policy, CN reserved as a hive name.
- The controller policy gains read on that path. swarm-controller reads it
  at start under its own certificate with the OIDC secret's retry, and runs
  without forge access if it still cannot.
- forgeTokenFile, its LoadCredential and the forgeEnv gate are gone;
  SWARM_CONTROLLER_FORGE_URL is always set. hostSwarmControllerTokenFile is
  removed, and a tmpfiles rule deletes the old host copy on the forge host.

The in-container file stays: the mint unit's only-once guard reads it, and
it is the publisher's source.

Refs #4998
2026-10-08 13:25:47 +02:00
atlas
1ca758daa7 swarm-controller grants internal/docs read to agents; hives stop doing it
Agent creation gains a GrantDocsRead node (after CreateForgeUser) that adds
the agent as a read collaborator on internal/docs, and forge::docs_grant
runs a pass over the store's hive-agent-* roster at start and every five
minutes that queues the same job for any agent that is not one. The pass only
adds; an unreadable roster or forge read changes nothing.

hive-c0re drops shared_docs_access, its call in sync_agent, and the
SHARED_ORG / SHARED_DOCS_REPO constants only it used.
2026-10-07 21:39:55 +02:00
atlas
21e3f626f4 hives read the forge through their own swarm-minted read account
swarm-controller now gives every hive in its directory a forge account,
`hive-<hive>`, in a new `hives` team of `agent-configs` (read on
`repo.code` + `repo.pulls`, every repo including later ones), and mints
one `read:repository` token named `swarm-hive` into the store at
`swarm/hives/<hive>/forge/token`. A five-minute pass queues a
`MintHiveForgeToken` node for a hive whose token is missing or no longer
matches the forge, whose account is missing, or whose team membership or
team settings drifted. A hive whose store or forge read failed is never
queued, so an outage cannot rotate every hive's token. The token decision
reuses `agent_token`'s classifier and replace step, now parameterised by
name and scopes.

An agent may be called `hive-<x>`. The hive account is created with
`hive-<hive>@hives.hyperhive.local` and the job refuses an account by that
name with any other email before it locks it down, adds it to the team or
mints for it; agent creation already refuses the hive's account by the
same email check.

The controller's bao policy gains the `swarm/hives/+/forge/token` leaf
(create/read/update), pinned by the bao-grants module-eval assertion. The
hive's own policy already reads `swarm/hives/<hive>/*`.

hive-c0re reads that token through `forge::reader_token()`, falling back
to the core token file when the store holds none, and uses it for:
fetching a merged config rev (swarm deploy, `hivectl forge
reconcile-config`), reseeding a purged `applied`, the meta-update
cascade's fast-forward, seeding `proposed` on first deploy, and finding
the PR that merged a refused rev. Those reads no longer require a local
`hive-forge` container. The `/knowledge` clone moves from the forge
host's boot sweep into the knowledge pull, so every hive clones it.

Posting the deploy-failure comment on that PR stays on the core token.

Refs #4851
2026-10-07 20:52:32 +02:00
atlas
d0c2b85d45 swarm-nats-auth: limit hive and swarm-controller subscribe to the subjects they use
A hive's queue credential and the swarm controller's could subscribe to
every subject in the swarm account, so a hive could read every other
hive's deploy requests and every agent's terminal, state, input and
request replies.

Their grants now carry a subscribe allow-list:

- a hive `hive-<h>`: `$SWARM.knowledge`, `$SWARM.deploy.<h>` and its
  reply inbox `_INBOX.client.hive-<h>.>`
- the reader (swarm-controller): `$SWARM.term.*`, `$SWARM.term.*.sub.*`,
  `$SWARM.agent-state.*` and its reply inbox
  `_INBOX.client.<client-id>.>`

`swarm_queue_client::connect`, which hive-c0re and swarm-controller both
use, now connects with `_INBOX.client.<client-id>` as its inbox prefix,
the same shape agents got for their own credential. In async-nats 0.50
every request reply and every ordered consumer (the hive's wanted-state
watch, the controller's KV key listings) delivers under that prefix, so
the one inbox grant covers them all.

A hive or controller still on a build without the prefix has every
JetStream reply refused once the responder runs this policy, so the
clients need to be rebuilt before, or with, swarm-nats-auth.

Closes #4945
2026-10-07 16:05:36 +02:00
atlas
6646329da5 swarm-nats-auth: read its own OIDC client secret from the store
The responder reads its OIDC client secret at
`swarm/services/<client-id>/oidc/client` with the store login it already
opens for the callout keys, and holds it in memory. `--client-secret-file`
is removed.

When introspection answers non-2xx, the responder re-reads the secret from
the store once and, if it changed, retries the introspection once. Both fit
inside the request's 1.5s introspection budget, under the server's 2s
callout timeout: the budget is now one deadline for the whole request. A
re-read that does not finish by then keeps running in the background, the
request is denied, and the next request introspects with what it read. One
re-read runs at a time; a refusal while one runs waits on it.

In swarm-nats.nix the `swarm-bao-nats-auth-oidc` fetch unit, the flag and
the `oidc-client.secret` LoadCredential entry are gone, along with the
unit's policy ordering in glue-bao-readers-policy-order.nix. A tmpfiles `r`
rule removes the copy a container still holds at
/var/lib/nixos-containers/swarm-nats/var/lib/swarm-nats-auth/oidc-client.secret;
nothing reads it.

No policy change: the `swarm-nats-auth` role already reads this path
(`natsAuthReaders` in swarm-bao.nix), which is the role the removed fetch
unit logged in as.

Deploy order: none, and no operator step. The store's grant already exists,
so the queue host can be deployed before, after or without the store's host
and authelia's. Rolling back re-creates the fetch unit and the file; the
store leaf is untouched.
2026-10-06 14:54:19 +02:00
atlas
a66670be87 hive-tls: the root fetch presents the hive identity, as on main
On a hive fronting no swarm name, swarm-services-cert's token-less
GET pki/ca/pem presents baoHiveIdentity{Cert,Key}File again. The fetch
never logs in and grants nothing; the client certificate only completes
the store listener's mTLS. A hive host without the store therefore needs
no copy of the services-issuer pair.

The issue path, which logs in to pki/issue/swarm-services, still presents
only deploy.bao.servicesIssuerClient{Cert,Key}File.

module-eval-hive-tls: the root-fetch cases pin the hive identity and the
absence of the services-issuer pair (a fixture holding both, and one
holding only the hive identity); the issue-path case pins the pair on a
hive host and on a host without a hive; a gateway without the pair
evaluates and its issue path names both options.

Root-fetch identity: option (a), mara on PR #5002 (comment 92591).

Refs #4921
2026-10-06 14:54:07 +02:00
atlas
6a9c819136 hive-tls: a gateway host without the services-issuer pair evaluates and goes red at runtime
Drop the eval assertion b78190e1 added. It refused every self-signed
gateway host that does not run the store, which is the default hive
shape, and contradicts docs/swarm/secrets.md's supported no-store
deployment. The approved #4921 plan has such a host evaluate and fail
at runtime; `requireServicesIssuer` already exits 1 naming both
options, on the root-fetch branch and before issuing.

The module-eval case that pinned `hiveWithController { }` as refused
now pins it, and a hive-less gateway with no pair, as evaluating with
the unit's refusal in the script. The fixture pairs the assertion
forced into forge-placement, nats-tls and swarm-otel-identity are
reverted.

Refs #4921
2026-10-06 14:54:07 +02:00
atlas
5761398d60 hive-tls: the gateway leaf uses the services-issuer identity, not a hive's
swarm-services-cert issues the swarm-services leaf a self-signed gateway
serves, and on a host fronting no swarm name fetches the store's pki root.
It is gated on the gateway, not on a hive, yet its leaf pair was named
under deploy.hive-controller.tls and defaulted from the hive reader's cert
directory, and the root fetch presented the hive's own identity. A gateway
host with no hive had no such identity, so that branch was red by
construction.

- The pair moves to deploy.bao.servicesIssuerClient{Cert,Key}File, beside
  the store's other host-unit readers, and glue-bao-tls.nix defaults it on
  the store's host from the leaf it already mints. The old
  deploy.hive-controller.tls.baoClient{Cert,Key}File paths are
  mkRenamedOptionModule aliases. glue-services-issuer-bao-identity.nix,
  which derived the default from deploy.bao.clientCertFile, is gone.
- The no-swarm-name root fetch presents the services-issuer pair. The hive
  identity is now only hive-tls-resign's.
- A self-signed gateway with no pair fails evaluation, naming both options.
  A pair named but unreadable still fails the unit, naming the paths.

Refs #4921
2026-10-06 14:54:07 +02:00
atlas
c10cc0e018 swarm-nats: take the callout issuer and user keys from the store
`swarm-bao-nats-callout-xkey` runs `ensure-callout-keys`, importing
/var/lib/swarm-nats-callout/{issuer,callout-user}.seed when present so the
keys keep their public halves, then fetches all three public halves from
the store. The verb removes each seed file once a read of the store returns
a seed; when the store's seed differs, it is kept and a warning names the
file. No tmpfiles rule touches those files, since one could run before the
import with the store sealed and leave the next run to mint a new key.

The server renders the issuer and user public halves from the fetched files,
checked for shape as the xkey's is, and does not start without them. The
responder reads both seeds from the store, so the copies into the container
and their LoadCredential entries are gone, and a tmpfiles rule removes the
copies a container still holds. `swarm-nats-callout-keys` is removed.

On the store's host the fetch unit is also ordered after
`swarm-bao-secret-publisher-policy`, so the publisher's denial of the seed
paths is written before the unit creates or keeps a seed there.

`deploy.nats.autoGenerateCallout`, `calloutUserPublicKey`,
`calloutIssuerPublicKey`, `calloutUserSeedFile` and `calloutIssuerSeedFile`
are removed with `mkRemovedOptionModule`, under `swarm.nats` too, each
message saying the keys live in the store.
2026-10-06 14:53:33 +02:00
atlas
1b18f6dcbd swarm-nats, matrix: the queue's and matrix-ctl's store identity paths are their own, not the hive's
glue-nats-bao-identity.nix and glue-matrix-ctl-bao-identity.nix defaulted
deploy.nats.baoClient{Cert,Key}File and deploy.matrix.ctlBaoClient{Cert,Key}File
to siblings of deploy.bao.clientCertFile, the hive's own store reader. That
pointed a store host whose reader was moved at files nobody mints, and on a
queue or homeserver host without the store that held a placed hive reader
invented a nats.pem / matrix-ctl.pem path beside it.

Both glues are now gated on the store running on this host
(deploy.bao.enable, the condition glue-bao-tls.nix mints under) and name the
directory the minting unit writes to, through nix/host-modules/lib/bao-pki-dir.nix.

Host shapes:
- Store host, default reader: unchanged. The evaluated paths stay
  /var/lib/swarm-bao-pki/nats{,-key}.pem and
  /var/lib/swarm-bao-pki/matrix-ctl{,-key}.pem, and the rendered units are
  the same.
- Store host whose hive reader was moved: now gets the leaf minted there.
- Queue or homeserver host without the store, holding a hive reader and not
  naming these options: no default any more. It still evaluates (neither
  option has an eval assertion). swarm-bao-nats-tls exits 1 naming
  deploy.nats.baoClient{Cert,Key}File when the queue's certificate is missing
  or due for renewal; swarm-bao-matrix-oidc exits 1 naming
  ctlBaoClient{Cert,Key}File, and the swarm appservice render/publish units
  and the PKI bind mount no longer render. If that host had placed nats.pem
  or matrix-ctl.pem beside its hive reader, it worked before and now needs
  both options named.
- Hosts without the store and without a hive reader, and hosts naming the
  options: unchanged.

Both refusals and option descriptions now say the default comes from
deploy.bao.enable.

Refs #4921
2026-10-06 12:04:48 +02:00
atlas
3c79c5eb5d swarm-nats: the auth responder's store identity path is its own, not the hive's
glue-nats-auth-bao-identity.nix defaulted deploy.nats.authBaoClient{Cert,Key}File
to a sibling of deploy.bao.clientCertFile, which is the hive's own store
reader. That tied a swarm service to a hive identity, pointed a store host
whose reader was moved at a file nobody mints, and on a queue host without
the store that held only a placed hive reader invented a nats-auth.pem path
that passed the eval assertion and failed at runtime.

The glue is now gated on the store running on this host (deploy.bao.enable,
the condition glue-bao-tls.nix mints under) and names the directory the
minting unit writes to, shared through nix/host-modules/lib/bao-pki-dir.nix.
A queue host without the store gets no default and the eval assertion
refuses it until the responder's own leaf is named; the assertion message
now names deploy.bao.enable as where the default comes from.

On a default store host the evaluated paths are unchanged:
/var/lib/swarm-bao-pki/nats-auth{,-key}.pem.

Refs #4921
2026-10-06 10:04:30 +02:00