Watch
0
0
Fork
You've already forked hyperhive
0
Commit graph hyperhive/docs/swarm
Author SHA1 Message Date
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
atlas
923554cd62 swarm-nats-auth: refuse an auth request the server did not seal
Every nats-server this module starts renders `auth_callout.xkey` or does
not start, and the responder serves only that server, in the same
container and generation. A request without `Nats-Server-Xkey` therefore
comes from no supported server, so the plaintext framing is gone: such a
request is logged at warn and gets no reply, and the server that sent it
times out the connect as a denial. A signed denial would have had to go
out unsealed, which is the path this removes.

Also scopes two claims to what is true: the xkey's public half is read by
the swarm-nats-auth policy only, and the metadata-delete gap in the
replace-key procedure is about the two xkey paths, not all of
`secret/metadata/`.
2026-10-05 23:25:36 +02:00
atlas
1ac34a4ad6 docs: callout xkey needs the store at every responder start; split-host order; replacing it
secrets.md now says that the responder reads the seed at every start, so
a store outage plus a responder restart refuses every client; that on a
split deployment the store's host deploys first; and that replacing the
key takes `bao kv metadata delete` on both paths, since a soft delete
keeps the metadata and the create is then refused. It also drops the
claim that every token reads `swarm/public/`.
2026-10-05 23:25:36 +02:00
atlas
06784e55fa swarm-bao: drop the default-policy grant; swarm/public/ is read by name
Writing bao's `default` policy needed a new stanza in the granter's own
policy, which only the bootstrap token can upload, so deploying it took
a manual root-token step. Nothing needed it: the one reader of the
callout xkey's public half is `swarm-bao-nats-callout-xkey`, which logs
in as the `swarm-nats-auth` role, and that role's policy already names
the path. `swarm-bao-nats-auth-policy` writes that policy as the granter
on every deploy, under the granter's existing `sys/policies/acl/swarm-*`
grant.

Removed: the `swarm-bao-public-read-policy` unit, its marker block, the
granter's `sys/policies/acl/default` stanza, the granter-role restart of
that unit, the bao.md section and the module-eval cases pinning them.
2026-10-05 23:25:36 +02:00
atlas
9a241a1df1 swarm-nats: fetch the callout xkey before the server starts and seal callout traffic
swarm-bao-nats-callout-xkey runs on the queue's host before the
swarm-nats container, under the responder's store leaf: it runs
`swarm-nats-auth ensure-xkey`, reads the public half back from the
store, and writes it where the container's runtime config dir is. On a
change it restarts the server.

nats-server now always starts through the runtime wrapper, in manual
mode too. A privileged ExecStartPre renders auth_callout with the xkey
(and, in auto mode, the two generated nkeys) at every start, and fails
the server when no xkey has been fetched. The auto-mode key unit now
only mints seeds and writes their public halves.

The responder is wanted by nats.service, so a server started late, once
its key arrived, brings the responder up with it.
2026-10-05 23:25:36 +02:00
atlas
bb571285fd swarm-bao: the responder may create the callout xkey; every token reads swarm/public/
The swarm-nats-auth role gains create+read on exactly the callout xkey's
two leaves, and no update, so a stored pair is never replaced.

swarm/public/ is readable by every token through bao's default policy.
swarm-bao-public-read-policy reads that policy, replaces only its own
marker-delimited block, and writes it back when it differs; it never
writes a policy it read back empty. Writing `default` is a new grant on
the granter's own policy, so on an existing store it lands only after
the bootstrap-token step re-runs; until then the unit says so and exits
0, and swarm-bao-granter-role restarts it on success.
2026-10-05 23:25:36 +02:00
atlas
b6f5cb0479 swarm CA: say when tmpfiles deletes the old CA files
`systemd-tmpfiles` removes them at boot and when a switch changes the
host's `tmpfiles.d` rules, not on every deploy.
2026-10-05 21:17:10 +02:00
atlas
626790a464 hive CA: catch a deleted CA file however its path is spelled
The CA-file assertion compared the option's raw string, so
`/var/lib//swarm-ca/root.pem` or `/var/lib/hive-tls/./ca.pem` got past it.
Both sides are normalised lexically now: empty and `.` components drop out,
`..` removes the one before it, a trailing `/` goes. A symlink is not
resolved at eval time, so a path through one still passes; docs/swarm/ca.md
says so.
2026-10-05 21:17:10 +02:00
atlas
1a4dc16053 hive TLS: reload the trust bundle's unit on a switch, name the missing root when it's empty
A switch that changed `hive-tls-ca` stopped and started it, and the stop
propagated through `Requires=` to the store's container (sealed again under
the shamir seal), every container binding the bundle, and every
`<name>-ca-bundle` unit with its consumers. `reloadIfChanged` with the same
script as `ExecReload` rewrites the bundle in place and restarts none of them.
`Requires=` stays, so a failed assembly still keeps them from starting.

With `swarm-services-root.pem` missing, `hive-tls-ca` writes an empty bundle
and every `<name>-ca-bundle` unit fails. Its log line now names the root's
path. docs/swarm/ca.md describes both.
2026-10-05 21:17:10 +02:00
atlas
cf7d357f17 swarm CA: describe the store's pki root as the only anchor in docs and comments
The docs, option descriptions and comments that named the swarm root, the
swarm CA or the hive CA as an existing trust anchor now name the secret
store's pki root or the trust bundle that holds it:

- docs/swarm/README.md, secrets.md, bao.md
- deploy.bao.clientCaFile's and gateway.verifiedProxyTo's descriptions
- comments in hive-c0re/src/meta.rs, hive-agent/src/swarm_queue.rs,
  swarm-logs/src/auth.rs, swarm-controller/src/vcs_metrics.rs,
  hive-forge, swarm-authelia, swarm-ci-runner, glue-bao-tls, swarm-bao,
  swarm-grafana, swarm-otel, otel, hive-c0re, lib/hive-ca-trust.nix,
  hive-forge/Cargo.toml and the flake-update workflow

Nothing behaves differently: every edit is a nix, Rust or TOML comment, an
option description, or a test assertion message. The Rust edits change the
workspace source, which this PR already rebuilds for hivectl.

Refs #4588
2026-10-05 21:17:10 +02:00
atlas
15f7d4d0cf hive TLS: re-issue a services leaf that does not chain to the store's root, delete the hive CA's leftovers
- swarm-services-cert (and its daily renewal) treats a services leaf that
  `openssl verify` rejects against swarm-services-root.pem as invalid and
  asks the store for a new one, as hive-tls-resign already does for the
  gateway leaf. The trust bundle is that root alone, so a leaf under any
  other issuer verifies for no consumer. With the store unreachable the
  unit fails, and where the gateway waits for the leaf, the import fails
  with it.
- Every host also deletes the old hive CA's ca.srl and .swarm-ca-adopted
  in deploy.hive-controller.tls.stateDir, with the same tmpfiles `r`
  rules as ca.pem/ca-previous.pem/ca-key.pem. Nothing reads either.
- docs/swarm/ca.md: the services leaf's re-issue conditions, the two
  extra deleted files, and that the deletion is irreversible: rolling back
  to a generation with swarm-ca.service mints a new, unrelated root that
  nothing issues under.

module-eval hive-tls: a case for the services-leaf check in the boot
issuance and the renewal (red with the line removed), and the deletion
case covers the two extra files.

Refs #4588
2026-10-05 21:17:10 +02:00
atlas
4e5a642928 hive CA: delete ca.pem, ca-previous.pem and ca-key.pem, the bundle is the store's root
The gateway leaf comes from the store's pki mount, so a hive CA's
certificates anchor nothing and its key is a secret outside the store.

- hive-tls-ca writes trust-bundle.pem from swarm-services-root.pem alone;
  the ca.pem and ca-previous.pem slots are gone.
- Every host deletes ca.pem, ca-previous.pem and ca-key.pem in
  deploy.hive-controller.tls.stateDir with tmpfiles `r` rules that follow
  the option. The removal shim, now host-ca-removed.nix, holds these
  beside the /var/lib/swarm-ca rule and the swarm.ca.* removals.
- deploy.bao.clientCaFile, deploy.bao.serverCaFile and
  deploy.swarm-controller.hiveClientCaFile naming /var/lib/swarm-ca or one
  of the deleted hive CA files fail evaluation, naming the store's mTLS CA
  (/var/lib/swarm-bao-pki/ca.pem on the store's host) as the replacement.

module-eval hive-tls covers the single-anchor bundle, the deletion (incl.
a relocated stateDir) and the assertion on all three options.

Refs #4588
2026-10-05 21:17:10 +02:00
atlas
93f1a76ea7 swarm CA: retire the host-held root CA, the store holds the only anchor
The secret store's pki root issues every hive's gateway leaf and the
swarm's service names, so the root swarm-ca.service generated on the
host anchors nothing and its key is a secret outside the store.

- swarm-ca.nix is gone: swarm-ca.service, root.pem and root-key.pem,
  and the swarm.ca.{autoConfigure,stateDir,validityDays} options. A
  definition of any of them fails evaluation with a removal message
  (swarm-ca-removed.nix), and a tmpfiles rule deletes /var/lib/swarm-ca
  on every host. singleHostSwarm no longer sets a swarm.* option.
- hive-tls-ca drops the $root slot: trust-bundle.pem holds the store's
  pki root and a hive CA's public ca.pem/ca-previous.pem only.
- hivectl peer-config no longer prints the scp line for the swarm root;
  the store's pki root is the one step.
- The gateway's cert import no longer stands a hive leaf or a throwaway
  in for a missing swarm-services leaf where it Requires= the issuing
  unit: it fails, naming swarm-services-cert.service. The stand-in stays
  where the gateway does not wait (no swarm name, or the store's UI
  only, whose passthrough must start with the store sealed).

Comments and docs that named swarm-ca.nix, swarm.ca.* or the host root
now name the store's pki root.

Refs #4588
2026-10-05 21:17:10 +02:00
atlas
8ffceea8e3 swarm-ci-runner: load the registration token in a register unit of its own
systemd hands a unit's credentials to every process of that unit, and a
host job is a process of the runner daemon, so the `LoadCredential=` on
gitea-runner-swarm still let any workflow read the registration token
from `$CREDENTIALS_DIRECTORY`.

swarm-ci-runner-register.service, a oneshot under the daemon's dynamic
user and state dir, now holds the only `LoadCredential=` and runs the
precond and `act_runner register` (only when `.runner` is missing). The
daemon unit has no credential, no `EnvironmentFile` and no `TOKEN`; its
pre-start step starts the register unit, so a missing token fails the
daemon's start and `Restart=on-failure` retries it.

The precond compares `.runner`'s labels in the form the daemon rewrites
them to (`nix` -> `nix:host`); compared verbatim, a scheme-less label
re-registered the runner on every start.

tmpfiles removes the unused `registration-token.env` and upstream's
`.token-hash`/`.labels` markers.
2026-10-05 16:33:24 +02:00
atlas
0302b29ca4 swarm-ci-runner: give the registration token only to the register step
The runner got its registration token through an `EnvironmentFile`
(`TOKEN=…`), so the daemon carried it for its whole life, and for a
`host` label act_runner copies its environment into every job step:
any workflow could read a token that registers runners on the forge.

The host unit now writes the bare token, and the runner unit loads it
with `LoadCredential=`. The upstream pre-start step, which registered
from `$TOKEN`, is replaced by one register step that reads the token
from `$CREDENTIALS_DIRECTORY` and runs `act_runner register` only when
`.runner` is missing, with the instance URL, name, labels and config
file upstream used. The precond now also removes `.runner` when its
labels differ from the configured ones, which upstream's step covered.
A changed token alone no longer re-registers.

Host jobs can still read `.runner` itself (#4979).

Closes #4974
2026-10-05 16:06:52 +02:00
atlas
e2da5509ae swarm-ci-runner: run the forge CI runner as a swarm service
The CI runner was a hive deploy module: hive-ci.nix ran it beside the
forge, and hive-c0re registered it through the forge's admin API with
the `core` token, writing the registration token through hive-priv. It
could only run on the forge's host, and it kept an admin-scoped forge
token on a hive.

Now the runner is a swarm service:

- swarm-ci-runner.nix runs it in a `swarm-ci-runner` container on the
  host that sets deploy.ci-runner.enable, reaching the forge by name.
  It needs neither the forge nor a hive there.
- swarm-controller reads the forge's runner registration token with its
  own admin token and writes it to swarm/services/ci-runner/
  registration-token whenever it differs from what it last wrote. Its
  grant on that path is create/update only.
- The runner's own leaf (CN swarm-ci-runner, role and policy of the same
  name) reads that one path. swarm-bao-ci-runner.service fetches it into
  the runner's EnvironmentFile and restarts the runner on a change.
- A pre-start step removes .runner when the daemon's journal since its
  last start holds Forgejo's "unregistered runner" refusal (the daemon
  exits 1 when its startup Declare gets it), or when .runner names
  another forge address; upstream's pre-start step then registers again.

Removed from the hive side: hive-ci.nix and deploy.forgejo.ci.*,
forge/ci_runner.rs, PrivRequest::RegisterCiRunner and its handler and
ReadWritePaths entry, InfraContainer::Ci with hivectl's --ci and the
dashboard's hive-ci journal entry, HYPERHIVE_FORGE_CI_ENABLED, and
read:admin,write:admin from CORE_TOKEN_SCOPES: nothing else in hive-c0re
calls /api/v1/admin/*. The old ci options, under both swarm.forge.ci and
deploy.forgejo.ci, now fail evaluation naming deploy.ci-runner.

The forge's half of the old ci.enable, the actions/checkout mirror and
DEFAULT_ACTIONS_URL = self, is deploy.forgejo.actionsFromSelf.

Closes #4954
2026-10-05 13:55:37 +02:00
atlas
962da3b784 swarm: OIDC clients registered wherever authelia runs; nats + matrix secrets from bao
The three client registrations move into glue-{controller,nats,matrix}-oidc-client.nix,
gated only on deploy.authelia.enable and reading their ids from swarm.* options,
so the host running authelia registers every one of them whichever host runs
the service. The matrix callback is a new read-only swarm.matrix.sso.redirectUri,
read by both the homeserver's config and its client entry.

swarm-secret-publish now also publishes the queue's and the homeserver's minted
secrets to swarm/services/<id>/oidc/client. The queue host reads its responder's
secret there with swarm-bao-nats-auth-oidc under the responder's own leaf
(swarm-nats-auth role gains that one read path), and the homeserver's host with
swarm-bao-matrix-oidc under matrix-ctl's leaf (swarm-matrix-ctl role gains its
one read path). The host-file copies are gone: swarm-nats-auth-secrets now
delivers only the callout seeds and the store identity, and
hive-matrix-oidc-secret is removed. A fetch unit whose leaf is unset still
renders and fails naming the option pair.

Closes #4943
2026-10-05 09:58:02 +02:00
atlas
48067d07e8 matrix: drop the per-hive appservice and its unused token
Every hive's homeserver loaded a per-hive `hyperhive` appservice
registration, and the swarm minted and delivered an as_token for it
through bao. No client presented that token: swarm-controller creates
every `@hive-<hive>` sender and every agent account with the swarm
appservice token (hive_sender.rs, agent_token.rs), and no Rust code read
the per-hive one.

Removed, with everything that existed only to serve it:
- hive-matrix.nix: the `hyperhive` registration, its activation-time
  render, the host bind mount of /var/lib/hyperhive/matrix-appservice,
  its LoadCredential entry, and the internal options
  `deploy.matrix.appserviceTokenFile` / `appserviceRegistrationScript`.
  tuwunel now loads only the swarm registration.
- glue-matrix-bao-token.nix and its `swarm-bao-matrix-token` unit.
- glue-bao-tls.nix: the matrix-token leaf and its defaults.
- swarm-bao.nix: the `swarm-matrix-token-<hive>` roles and policies
  (`swarm-bao-matrix-token-policy`), and the options
  `deploy.bao.matrixTokenClient{Cert,Key}File` and
  `matrixTokenCommonNamePrefix`, now mkRemovedOptionModule.
- swarm-secret-publisher.nix: the per-hive token mint and its state dir.
- swarm.nix: the reserved `<matrixTokenCommonNamePrefix>-<hive>` CN.
- swarm-secret-client: `matrix::appservice_token_path`.

The swarm registration (swarm-matrix-ctl, swarm-controller) is untouched.
Comments and docs that pointed at the removed glue file now point at
glue-queue-agent-credential.nix, which has the same reader shape.

Refs #4428
2026-10-05 07:48:17 +02:00
atlas
e4b170f8c6 docs: the hive has no approval queue
config-changes.md (was approvals.md) describes config PRs, the meta flake
and helper events; the approval queue, its kinds, the withdraw path, the
job-queue dispatch and ApprovalResolved go. Every other page drops the
approvals table, endpoints, event, tool group, hivectl verbs, dashboard
section and the loose-end/cancel kind, and links to the renamed page.
2026-10-04 23:55:32 +02:00
atlas
2505008b83 swarm-secret-client: hives no longer read swarm/services/*
The hive policy rendered by policy::render granted every hive cert read
on secret/data/swarm/services/*, which holds each swarm service's OIDC
client secret. No reader of that tree authenticates with a hive cert:
grafana, the otel collector and the store forwarder each read with
their own leaf, and the bao UI's secret is read by the store's granter.

render() now emits two stanzas: swarm/agents/* and the hive's own
swarm/hives/<hive>/*. Kind::Service had no other caller and is removed.
Docs and comments that described the grant as a decision are rewritten
to the current state.

Closes #4960
2026-10-04 23:11:11 +02:00
atlas
91fdf96a67 docs: drop harness systemd settings; swarm-nats-auth: refuse the hive's shared agent client
Docs (review on #4948): the harness's restart settings are not useful to
a reader, so the `Restart`/`RestartSec` bullet in docs/turn-loop and the
"30 seconds later, no start limit" wording in docs/swarm are removed.
What stays is the behaviour: the harness exits and comes back until the
queue accepts it.

Grant: with the harness's shared-client fallback gone, nothing presents
the hive's shared agent client (`hive-<hive>-agent`) to the queue;
swarm-logs uses that OIDC client over HTTP only. swarm-nats-auth now
refuses it outright instead of granting the configured
`$SWARM.term.{hive}.>` / `$SWARM.agent-state.{hive}.>` publish and its
`_INBOX.client.<id>.>` subscribe, so an agent holding it can no longer
publish as a sibling. The agent arm still runs before the hive arm, so
that id never falls through to its hive's grant.

Removed with it: the `--agent-publish-subject` flag and its two uses in
swarm-nats.nix, `Policy`'s agent subject list, and
swarm-queue-client's `client_inbox_prefix`. The module-eval now asserts
the flag is absent; the policy tests assert the refusal and that the
hive itself is still admitted.
2026-10-04 22:24:51 +02:00
atlas
235bc481b3 hive-agent: the swarm queue connection is required; retry, then exit
An agent's harness connects to the swarm queue with the agent's own
credential or not at all. The first connect retries after 5s, doubling
(7 attempts, about five minutes), with a warning per failed attempt;
when every attempt fails the harness logs an error and exits 1. Missing
coordinates (HIVE_AGENT_NATS_URL, or the secret store's BAO_ADDR /
HIVE_AGENT_NAME / certificate) fail the boot outright.

Previously the first connect's outcome was memoized for the life of the
process: a store 403 or queue refusal at boot left the agent on the
hive's shared client, or disconnected, until someone restarted it, and
an agent with no queue coordinates ran on without one. Both paths are
gone, with the hive-scoped subjects that only the shared client used.

The harness unit now restarts after 30s instead of 2s, so a swarm-wide
outage doesn't restart every agent every 2s, and has no start limit, so
it keeps coming back however long the outage lasts.

The hive's shared client credential is still imported into the harness
unit for swarm-logs; the two HIVE_AGENT_OIDC_*_FILE variables that
pointed the harness at it are dropped, as is the unset
HIVE_AGENT_OIDC_CA_FILE override.

With the fallback gone, the pieces #4944 added only for it go too:
swarm_input's skip-on-shared-client branch and swarm-queue-client's
connect_with_inbox_prefix. swarm-nats-auth's grant for the hive's shared
agent client is unchanged.

Closes #4909
2026-10-04 22:24:51 +02:00
atlas
b4a2aeb5ba docs: scheduled prompts are swarm-level; the hive page is a read-only list
Drops docs/tools/scheduling.md and every description of hive-level
schedule creation, the worker, the removed MCP tools, the scheduling tool
group and the removed endpoints/events, pointing at docs/swarm/schedules.md.
2026-10-04 21:23:31 +02:00
atlas
d81fb2236f swarm: grant the controller STREAM.MSG.GET; list subagents concurrently
GET /api/agents/{name}/subagents reads each subagent's newest row with
get_last_raw_message_by_subject, which publishes
$JS.API.STREAM.MSG.GET.term-sub-<agent>. The controller's NATS grant had
no STREAM.MSG.GET, so every lookup was refused and waited the 10s request
timeout, one subagent after another; six subagents passed nginx's 60s.

Grant `$JS.API.STREAM.MSG.GET.*`. A wildcard matches a whole subject
token only, so a `term-sub-*` spelling would name one stream literally;
`*` is the narrowest grant that covers the per-agent streams, the same
width as the controller's STREAM.INFO.* grant.

The lookups now run concurrently (16 at a time) with a 2s timeout each.
A lookup that fails or times out lists its subagent with a null
last_row_unix, and one warn per request names how many failed and the
first error.
2026-10-04 20:03:08 +02:00
atlas
01ec29da48 hive-gateway: no hive-domain vhost on a host without a hive
The hive-domain vhost renders only where a hive runs. On a host without
one the only certificate is the swarm-services leaf, which does not name
the hive domain, so a homeserver host without a hive serves matrix on the
homeserver's own name only; apex matrix discovery is #4878.

Comments and test names describe the code's current behaviour.

Refs #4588
2026-10-04 19:45:14 +02:00
atlas
3e1ac4e678 hive-gateway: keep matrix discovery on the hive domain of a homeserver host without a hive
The hive-domain vhost renders where a hive or the homeserver runs. Its
matrix .well-known JSON and /matrix/ redirect are served wherever the
homeserver runs; the dashboard, swagger UI, agent routes and the
agents.conf include only where a hive runs, since they proxy to hive-c0re.

The gateway doc's vhost map says which rows exist without a hive, and the
trust-bundle text describes ca.pem by what it is rather than as retired.

Refs #4588
2026-10-04 19:45:14 +02:00
atlas
60268b70b0 hive-tls: a store-only host without a hive starts its gateway with the store sealed
A gateway host without a hive whose only swarm name is the store's UI no
longer waits for swarm-services-cert: its nginx carries the stream
passthrough to the store and must come up while the store is sealed. Until
the services leaf lands, hive-gateway-self-signed-cert installs a throwaway
self-signed pair on the default and swarm-services paths there; on a host
without a hive it never references the hive leaf.

A host without a hive also no longer mints the hive-leaf identity or
defaults the options pointing at it, and does not render the hive-domain
vhost, whose upstream is hive-c0re.

Refs #4588
2026-10-04 19:45:14 +02:00
atlas
75a7dccee6 hive-tls: no hive leaf or hive identity on a gateway host without a hive
A self-signed gateway on a host with deploy.hive-controller.enable off
(a dedicated swarm-services host) needed the hive leaf for its catch-all,
and that leaf now comes only from hive-tls-resign with a per-hive store
identity. hive-tls-resign and its timer are now defined only where a hive
runs; elsewhere hive-gateway-self-signed-cert installs the swarm-services
leaf as the default cert, and the gateway waits for swarm-services-cert.
Also corrects the hive-gateway comments and option text that still said
the leaf is hive-CA-signed.

Refs #4588
2026-10-04 19:45:14 +02:00
atlas
fc33a62bf8 hive-tls: keep ca-key.pem on disk, and re-issue a leaf that does not match its key
hive-tls-ca no longer deletes a ca-key.pem an earlier generation left:
nothing reads it, and leaving it means a rollback finds the old key instead
of minting a new anchor. Deleting it moves to PR 3, beside dropping ca.pem
from the trust bundle.

hive-tls-resign now also re-issues when the leaf's public key differs from
the key file's. The leaf and key are moved into place by two renames, and an
interrupted pair left a certificate beside the wrong key that the validity
check accepted.

Refs #4588
2026-10-04 19:45:14 +02:00
atlas
5526daab97 hive-tls: issue the gateway leaf from bao with a per-hive identity
Every hive gets its own store identity (CN
<deploy.bao.hiveLeafCommonNamePrefix>-<hive>) and its own role on the
existing pki mount (<deploy.bao.hiveLeafPkiRolePrefix>-<hive>), whose
allowed_domains is the hive's domain and *.<domain>. The policy grants
update on that one issue path. swarm-bao-hive-leaf-policy writes role,
policy and cert-auth role for every hive in swarm.hives; the store's host
mints its own hive's identity in glue-bao-tls.nix and a new glue file
points deploy.hive-controller.tls.baoHiveIdentity{Cert,Key}File at it.
Hives on other hosts place it by hand. swarm.nix reserves the composed CN.

hive-tls-ca no longer generates a hive CA or signs a leaf: caGenScript,
signLeafScript/signHiveLeaf and the adoption block are gone, ca-key.pem
is deleted where it exists, and the unit only assembles trust-bundle.pem.
The retired ca.pem/ca-previous.pem stay in the bundle so leaves they
signed keep verifying.

hive-tls-resign becomes the bao issuance: at boot, on deploy and from a
daily timer it re-issues when the leaf is missing, past half the role's
lifetime, missing a name, or not chained to the store's pki root. Nothing
orders the gateway after it, so a valid leaf on disk never waits on the
store; a failed renewal leaves the leaf in place. A missing identity
fails the unit naming both options, as does the root fetch on a hive
fronting no swarm name, which now presents this identity instead of the
reader cert.

caValidityDays and leafValidityDays are removed (both spellings), since
nothing reads them.

Refs #4588
2026-10-04 19:45:14 +02:00
atlas
52037133e2 swarm-nats-auth: limit agent subscribe to its own inbox and input
An agent's queue credential could subscribe to every subject in the swarm
account, so any agent could read every other agent's terminal, turn state,
operator input and request replies. Agents are the least trusted clients
on the queue (mara's ruling on #4901), so their grants now carry a
subscribe allow-list:

- an agent's own credential (the harness and its subagent daemon):
  `_INBOX.agent.<agent>.>` and `$SWARM.input.<agent>`
- the hive's shared agent client: `_INBOX.client.<client-id>.>`

Hive and swarm-controller grants are unchanged (no `sub` claim, so still
every subject).

The clients connect with the matching inbox prefix, so the replies they
rely on still arrive: the icon write's JetStream ack and the schedule
DIRECT.GET. `connect_with_token` takes the agent name and sets the prefix
itself; the hive-shared fallback uses the new
`connect_with_inbox_prefix`.

The harness no longer subscribes to its input subject when it fell back
to the hive's shared client: that identity names no agent, so it cannot
be granted one agent's input without all of them. It logs that and
skips, the same as the schedule reader on that path.

Refs #4901
2026-10-04 19:22:41 +02:00
atlas
bed4358e19 swarm options: no swarm.* value depends on a deploy option or this host's hive
- swarm.serviceDomains lists every swarm service name on every host. The
  store's issuing role is built from it, so the store now admits a leaf
  for grafana, metrics, otel, logs, the bao UI and the apex whichever host
  runs the store. Each gateway's own leaf stays narrowed by
  localServiceDomains.
- swarm.matrix.apiUrl defaults to http://<swarm.matrix.gatewayHost>
  (null when gatewayHost is null) instead of loopback on the matrix host.
- The authelia vhost proxies /bridge/ to swarm-authelia-bridge with the
  prefix stripped, and deploy.swarm-controller.authBridgeUrl defaults to
  ${swarm.authelia.url}/bridge on every host. swarm.authelia.bridgeUrl is
  removed.
- swarm-authelia-bridge admits only swarm-controller's OIDC client on
  every request (403 for any other active token), configured by
  SWARM_AUTHELIA_BRIDGE_CONTROLLER_CLIENT_ID (renamed from
  ..._OPERATOR_READER_CLIENT_ID, which gated ListOperators alone).
- swarm.peerHives is removed; swarm-wireguard.nix filters this host's own
  hive out of swarm.hives itself.

Closes #4933
2026-10-04 18:13:47 +02:00
iris
8a9e199bad docs: schedules page section passes the prose lint 2026-10-04 17:49:42 +02:00
iris
0af7f53e88 swarm-ui: schedules page says what a past start does, no longer claims a one-shot fired
A start in the past fires the latest missed time once, right away, so the
form says that instead of 'already passed'. A one-shot past its time reads
'past' / 'time passed' (the controller's clock, not a record of firing) and
keeps its pause button. The edit form compares against what it opened with,
so a schedule changing under it is not reverted on save.
2026-10-04 17:49:42 +02:00
iris
63ee6d12be swarm-ui: a schedules page, one card per scheduled prompt
/schedules lists the controller's scheduled prompts as cards: name as the
title, the agents it fires for, the message, its cadence and next fire.
Edit turns a card into its own form; pause, resume and remove act in place,
and + schedule opens an empty form. The form sends only what changed, so a
rename never moves the fire times, and warns when the start or the repeat
does. Filters reuse the shared card filter bar.

Adds a TextAreaField to the ui kit, and an upcoming mode on RelativeTime so
a next-fire time reads "in 5m". Needs the controller's /api/schedules
routes.
2026-10-04 17:49:42 +02:00
atlas
a0b1846006 swarm: named scheduled prompts for one or more agents, fired in each agent
swarm-controller keeps every scheduled prompt (id, name, agents, body,
anchor, interval, paused) as one document under one key of a new
`schedule-definitions` KV bucket, so each change is a single
compare-and-set and a prompt exists for all of its agents or none. It
writes each agent's key of a new `agent-schedules` bucket as the prompts
naming that agent, without names or other agents, after every change and
on a one-minute reconcile pass. It is the only writer of both, behind
GET/POST /api/schedules and PATCH/DELETE /api/schedules/{id}.
`swarmctl schedule add|list|edit|pause|resume|rm` drives those routes over
the controller socket.

An agent's own queue credential is granted one more subject, the direct
get of its own key in `agent-schedules`. A KV watch is not granted: its
consumer create subject carries no key, so it would cover every agent's.

hive-agent's new schedule ticker reads that key every 10 seconds and fires
due slots as the same `schedule:<id>` todo hive-c0re's worker pushes. Fire
times are anchor + k*interval; the last fired slot per schedule id is kept
in the state db, so a body edit or list refresh never moves or refires a
slot, downtime fires the latest missed slot once, a restart refires
nothing, a one-shot fires once, and paused slots are skipped.

Refs #4914
2026-10-04 17:24:57 +02:00
iris
84c89de71c swarm-ui: show how recent each subagent is in the details panel
The subagent list endpoint now returns {name, last_row_unix} per
subagent, the publish time of its newest terminal row, read from the
queue controller-side so agents publish nothing new. The panel sorts
newest first, shows a ticking age beside each name, and dims one quiet
for 15 minutes.

Closes #4928
2026-10-04 17:24:25 +02:00
atlas
9e7fdb55ae swarm-controller: make authelia's operators matrix homeserver admins
swarm-authelia-bridge gains ListOperators, the usernames in authelia's
operator group (`operatorGroup = "admins"`, passed in from
swarm-authelia.nix). Unlike the two agent verbs, which admit any active
token, it answers only the client named by
SWARM_AUTHELIA_BRIDGE_OPERATOR_READER_CLIENT_ID (swarm-controller's
queueClientId) and returns 403 to every other active token: authelia
issues machine tokens to every hive and its agents as well. The bridge's
introspection now models `client_id` to make that check, the same way
swarm-nats-auth's does.

swarm-controller runs a grant-only pass at start and every five minutes:
read the group, map each username to the account an SSO login lands on
(tuwunel lowercases `preferred_username`), skip anyone already joined to
`#admins:<server>` (tuwunel's definition of admin) or without a matrix
account, and send `!admin users make-user-admin <user>` there as @swarm
with the swarm appservice token. Nothing is ever revoked: tuwunel has no
command for it (#4799).

The admin-room calls (alias, joined members, profile, send) go into
swarm-matrix-client next to whoami, keeping its no-body-in-errors rule.

Closes #4585
2026-10-04 16:45:08 +02:00
atlas
849bf46550 hive-tls: fail swarm-services-cert when the store identity is absent
A hive fronting no swarm name fetches the store's pki root presenting its
reader certificate. When deploy.bao.clientCertFile or clientKeyFile is unset,
or either named file is not readable, the unit now exits 1 with a message
naming both options and their paths, instead of exiting 0 with the root
missing from the trust bundle.

The module-eval cases that asserted the quiet skip assert the failure
instead, and a new case covers the named-but-absent files.
2026-10-04 14:37:50 +02:00
atlas
56335b24c0 hive-tls: skip the store root fetch on a hive with no store identity
The `want_svc=0` root fetch presents `deploy.bao.clientCertFile`/
`clientKeyFile` to a listener that demands a client certificate. On a
hive with neither set, the handshake is refused every time, and
`swarm-services-cert` went from green to failed-and-retrying for 24h.

The branch is now chosen at eval: with no reader identity, the unit
logs that the store root was not staged and names the option, then
exits 0, and no fetch is rendered. A hive with an identity fetches as
before. `hivectl peer-config` already handles the absent file.

Refs #4588 (review c88774, point 2).
2026-10-04 14:37:50 +02:00
atlas
72f5e39278 hive-tls: stage bao pki root in every hive's trust bundle
A self-signed hive fronting no swarm service name never filled
`$svcroot` (/var/lib/hive-tls/swarm-services-root.pem): the only writer
was `swarm-services-cert`'s issuance, which exits early on
`want_svc=0`. So those hives' trust-bundle.pem lacked the store's `pki`
root, even though every other hive's swarm-service names chain to it.

On `want_svc=0`, `swarm-services-cert` now reads the root from the
store's unauthenticated `GET /v1/pki/ca/pem`, over the hive's existing
BAO_ADDR, `deploy.bao.serverCaFile` and reader identity
(`deploy.bao.clientCertFile`/`clientKeyFile`) at the TLS layer only; no
login. It writes `$svcroot` and restarts `hive-tls-ca` when the root
changed, as the issuing path already does. The daily
`swarm-services-cert-renew` timer runs the same script, so the copy
follows a root rotation.

The gateway no longer `Requires=` `swarm-services-cert` on such a host:
nginx reads nothing that unit writes there, and the fetch retries
against a sealed store.

`hivectl peer-config` prints the root as step 2 of the self-signed
instructions: the swarm's service names are issued under it, not the
swarm root.

No new option. Refs #4588.
2026-10-04 14:37:50 +02:00
atlas
48ac4ac423 swarm UI: send a message to an agent, without its hive
The agent view in the swarm UI gets a message box. The text reaches the
agent's harness straight from swarm-controller over the swarm queue and
becomes an inbox turn from the operator's authelia username. hive-c0re is
not in the path.

- gateway: `/api/` forwards authelia's `Remote-User`, replacing any the
  client sent.
- swarm-controller: `POST /api/agents/{name}/input` takes that header as
  the sender (401 without it) and sends `{from, body}` as a NATS request
  on `$SWARM.input.<agent>`. No subscriber is a 503.
- swarm-nats-auth: the controller is granted publish on `$SWARM.input.*`;
  no hive or agent grant names the family, so the harness can trust
  `from`.
- hive-agent: a task subscribes to the agent's own input subject and
  pushes each message into the serve loop's existing reminder channel,
  renamed `local_msg_*` since it now carries both.

The harness does not reply to the request: no agent grant covers a reply
subject. The controller therefore answers 202 once the request times out
(2s) and 503 on the server's no-responders status.

Closes #4828
2026-10-04 03:02:14 +02:00