A declared agent that never reported (e.g. its first deploy was lost) had no hive in the roster row, so the wanted merge skipped it and swarm-ui showed 'no declaration' with the state control disabled. GET /api/agents/status now takes such a row's hive from the one hive whose declaration places the agent, so the declared state shows and the control works on deploy.
read_with_startup_retry retries the first read of a client secret through a store that is restarting. Nothing in swarm-queue-client calls it; its only callers are the four binaries that read the secret (swarm-builder, swarm-claude-login, swarm-controller, swarm-review-classifier), and they already depend on swarm-secret-client. It sits beside stored_secret, which moved there in #5538. swarm-queue-client drops its backon dependency and the tokio test-util dev-dependency that only these tests used.
no behaviour change: a pure move of items and tests into new modules, plus
visibility edits (pub(crate)) and path updates in sibling modules that named
crate-root items. same routes, same OpenAPI, 561 tests before and after.
why: #5470, new features stop landing in one 6.4k-line file by default.
files outside swarm-controller/src: docs/agent-lifecycle/agent-roster.md,
swarm-ui JobsPage.tsx and AgentsPage.tsx, nix/host-modules/swarm-controller.nix
and swarm-grafana.nix hold comments that named main.rs for items that moved.
The agent's state/AGENTS.md stays the truth and its harness the only
writer. The harness mirrors the file into a new `agents-md` KV bucket,
and swarm-controller serves that copy, so a read works while the agent is
stopped. An edit is a unified diff plus the revision it was made against,
sent over the agent's existing control subject; the harness refuses a
stale base, applies the diff with `git apply --check` then `git apply` to
a temp copy, and writes the file only when both pass. Fixes#5519.
Files:
- swarm-queue-client/src/agents_md.rs, lib.rs: the bucket name and
subject, the mirror value, the `ApplyAgentsMd` control command and its
reply. `AgentControl` loses `Copy` because the new variant carries text.
- hive-agent/src/swarm_agents_md.rs, main.rs: the mirror task (start,
10 s re-read, immediate republish after an edit) and the apply.
- hive-agent/src/swarm_input.rs: the control loop answers both commands.
- hive-agent/src/claude_md_watch.rs: its AGENTS.md path is shared.
- hive-agent/Cargo.toml, Cargo.lock: sha1 for the git blob id (already in
the lock), tempfile moves from dev to runtime for the apply directory.
- swarm-controller/src/agents_md.rs, main.rs: the bucket reader and
GET/PUT /api/agents/{name}/agents-md; test AppState literals get the
new field.
- swarm-controller/src/agent_control.rs: the sink hands back raw reply
bytes, so each command decodes its own reply type.
- swarm-controller-api/src/agents_md.rs: the route bodies, shared with
swarmctl.
- swarmctl/src/agents_md.rs, main.rs: `swarmctl agent agents-md get|apply`.
- nix/host-modules/swarm-nats.nix, nix/module-eval/nats-authelia.nix: the
agent's publish grant on its own `$KV.agents-md.<agent>` key.
git needs no new package: agent containers enable programs.git, and the
harness unit's PATH includes /run/current-system/sw.
The module is a rename (97% similar); everything else in the diff follows
from the move:
- stored_secret.rs: private `chain()` helper, the 8-line copy of the queue
crate's `chain` the module called as `crate::chain`; the queue crate's
public `chain` stays.
- stored_secret.rs: `Duration::from_mins(5)` in `MAX_REREAD_DELAY` is
unchanged moved code, not a new use.
- swarm-secret-client Cargo.toml: gains futures-util, tokio and tracing as
dependencies and tokio `test-util` plus tracing-subscriber as dev
dependencies; the module and its tests use them.
- swarm-queue-client Cargo.toml: drops futures-util and the tracing-subscriber
dev dependency, which only the moved module used; its tokio dev comment is
reworded to the tests that remain.
- Cargo.lock: the same dependency moves, nothing else.
- swarm-queue-client lib.rs: imports `swarm_secret_client::stored_secret`,
and its tests use a small local `Store` in place of the moved module's
test-only `Fake`, which is no longer reachable.
- callers (hive-c0re, swarm-builder, swarm-ci-metrics, swarm-claude-login,
swarm-controller, swarm-nats-auth, swarm-review-classifier): import path
only; no re-export is left behind.
The rustls provider install is process setup every swarm binary runs, and
swarm-secret-client depends on no other swarm crate, so a binary that only
needs the provider and the store no longer links the queue client.
- swarm-secret-client/src/lib.rs, Cargo.toml: the function, its test, a
`//!` line on why it lives there, and the rustls dependency; rustls
`ring` as a dev-dependency feature, so the test builds both providers as
the binaries do and fails if the install is dropped.
- swarm-queue-client/src/lib.rs, Cargo.toml: function and test removed;
`connect()` calls the secret client's; rustls dropped (no other use),
swarm-secret-client added.
- swarm-metrics-auth/Cargo.toml: drop swarm-queue-client, whose only use
was the provider; the binary no longer links async-nats.
- swarm-logs-auth, swarm-logs, swarm-metrics, swarm-ci-metrics Cargo.toml:
the dep comment no longer names the provider (they keep the dep for
agent_token / the OIDC mint).
- Cargo.toml: the rustls comment names the provider's new home.
- 14 call sites (hive-agent, hive-c0re, hive-subagent-mcp x2,
swarm-builder, swarm-ci-metrics, swarm-claude-login, swarm-controller,
swarm-logs-auth, swarm-logs, swarm-metrics-auth, swarm-metrics,
swarm-nats-auth, swarm-review-classifier): one-line path change.
- Cargo.lock: the dependency edges above.
A subagent's terminal opened directly showed nothing until the subagent
published again: the relay was a core subscription, though its rows sit
in term-sub-<agent> for 24 hours. It now goes through the agent route's
replay-then-tail, so it opens with the subagent's newest 100 rows and
resumes after Last-Event-ID.
- term_stream.rs: the replay takes a stream name and a filter subject
instead of an agent, and `replay_or_live` (replay, else the live core
subscription) is the one fn both routes call. When the filter is one of
several subjects in the stream, a fresh reader's consumer starts at the
oldest row and drops all but the newest REPLAY_ROWS it counts as
pending, since the stream's newest sequences may belong to sibling
subagents. A single-subject stream (term-main-<agent>) picks its start
with replay_start exactly as before. Tests for both subagent cases.
- subagent_term.rs: the relay calls replay_or_live on the subagent's
subject in term-sub-<agent> and reads Last-Event-ID; module doc and
OpenAPI text no longer say "live only".
- main.rs: the handler test passes the new headers argument.
- swarm-queue-client agent_term.rs: replay_consumer_config takes the
filter subject instead of the agent.
- swarm-queue-client subagent_term.rs: consumer_create_grant, the
extended CONSUMER.CREATE form with a subagent subject as filter.
- swarm-nats-auth policy.rs: the controller holds that grant; the agent
grant's `$SWARM.term.*` filter is two tokens short of a subagent
subject, and the test that refused it now admits it.
- docs/swarm/ui.md, README.md: a subagent terminal opens with its newest
rows.
Fixes#5527
The swarm UI's broadcast page (#5524) needs the controller to do the
fan-out, so POST /api/agents/broadcast takes {agents, body} and answers
200 with {results: [{agent, sent, reason?}]} in request order.
- agent_input.rs: the single-agent send is now send_one, which both
routes call; the single route maps its outcome to the same statuses
and texts as before. The broadcast validates the whole request first
(sender, empty body, empty or duplicated agents, more than 64, a name
outside the roster), then sends up to 16 at once with the single
route's no-responders window.
- main.rs: wires the route.
- docs/swarm/ui.md: describes the route next to the single-agent one.
/builds lists the builder's recent builds newest first, one card each with how
it ended, and opens a failed build's nix tail. /builds?run=<id> opens that
build's card and scrolls to it. The swarm-builder commit status now links
there instead of to the raw JSON record.
swarmctl decoded POST /api/agents and POST /api/agents/{name}/forge-token
into hand-written copies of the controller's private response structs.
Both now live in swarm-controller-api, so a field renamed on one side is a
compile error on the other. Wire format and OpenAPI schema are unchanged.
The request side stays mirrored: CreateAgentRequest carries inference
settings and an ArchetypeRef (swarm-queue-client types) and Permission
(hive-sh4re), and the permissions body is a list of Permission. Where
those types live is #5471's decision.
Files:
- swarm-controller-api/src/agents.rs, lib.rs: the two responses, with
round-trip tests whose literals come from the tests that move here.
- swarm-controller/src/main.rs: imports them; the struct definitions and
create_agent_warning_tests (now in the api crate) go.
- swarmctl/src/agent.rs: imports them; the mirrors and their decode tests
go, and the module doc says only the request is mirrored now.
- swarmctl/src/permissions.rs: its doc pointed at agent.rs for why it
mirrors; that reason no longer covers Permissions, so it gives its own.
- swarmctl/README.md, swarm-controller-api/README.md, CLAUDE.md: which
bodies are shared and which are mirrored.
Part of #5472
swarmctl hand-mirrored the controller's forge make-admin response and
the /api/schedules bodies. Both now import them from a new serde-only
crate, swarm-controller-api, the way hivectl and hive-c0re share
hive-host-sock. ToSchema sits behind its schema feature, which only
the controller turns on.
The wire format is unchanged: round-trip tests in the new crate pin
each type against the JSON the controller and swarmctl produced, and
the controller's OpenAPI document for these routes is byte-identical.
Files: swarm-controller-api/ is the new crate; swarm-controller and
swarmctl (src, Cargo.toml, Cargo.lock, root Cargo.toml) switch to it;
CLAUDE.md lists the crate and which swarmctl shapes are still local
mirrors; swarmctl/README.md says the forge and schedule bodies come
from the crate, so none of its lines still call them mirrored.
Part of #5472.
The hive reads the agent-archetype key when it provisions and rebuilds an
agent and keeps it in swarm-agent-archetypes.json. For an archetype agent
the meta flake has no agent-<name> input and mkAgent imports
hyperhive.hyperhiveArchetypes.<name>.module; provisioning, rebuilds, the
meta-input cascade, a deploy's rev and the startup migration skip the
config repos. The harness gets HIVE_ARCHETYPE and the system prompt says
the agent has no config repo. The create form sends the picked archetype.
Adds the android-dev built-in.
Refs #5386
POST /api/agents takes an archetype the stored list holds, records it in
the new agent-archetype bucket, and queues no CreateRepo, AddRepoMember or
InitAgentConfigRepo for it. A destroy clears the key. Hives may read the
bucket.
Refs #5386
The dashboard vhost, the hive domain vhost and the dashboard's local
name move to hive-c0re/vhosts.nix; the matrix .well-known discovery and
the legacy /matrix/ redirect move to glue-matrix-hive-discovery.nix,
which adds them to the hive domain vhost. Both declare through
gateway.vhosts. hive-gateway/vhosts.nix keeps the catch-all only, and
the gateway no longer reads deploy.matrix or c0re.servedFrontend, nor
deploy.hive-controller.enable for its vhosts.
Part of #3742.
The agent read the operator-profiles bucket with a keys() listing, an
ordered consumer whose grant (STREAM.INFO, CONSUMER.CREATE) and flow-control
reply had never run against a permissioned queue. The controller now writes
the whole list as one JSON array under one key, only when it changed, and
the agent reads it with one DIRECT.GET the way agent_inference::read_own
reads its key. The agent grant shrinks to that one subject.
swarm-controller reads each member of authelia's operator group's forge
profile every five minutes and keeps one key per operator in the new
operator-profiles bucket. Every agent may list and read that bucket, and
hive-agent renders it into the system prompt at harness start, falling
back to today's prompt when the bucket is empty or doesn't answer within
five seconds.
Fixes#5383
A flake declares agent archetypes in its `hyperhiveArchetypes` output,
`<name> = { module; meta; }`. The hyperhive flake ships `reviewer`
(default permissions without `execution`).
swarm-builder answers `$SWARM.builder.archetypes` with the `meta` of
every archetype its own hyperhive source declares. swarm-controller asks
once the queue connects and every 10 minutes, checks each archetype as
`POST /api/agents` checks its settings, and stores the result in the
`agent-archetypes` KV bucket when it changed. `GET /api/archetypes`
serves it, and the create form's archetype select prefills permissions,
resource caps and inference from it.
The agent created is an ordinary agent with its own config repo; the
archetype's module is not applied yet.
Refs #5386
A review request for a head whose newest recorded review approved or
requested changes now runs nothing: the classifier account posts that
verdict and table again, opening "Already reviewed <sha>, verdict
unchanged". A head whose newest review had no clean verdict, a failed
post, or no record gets a fresh run, as before.
An accepted request now keeps its key until its record is written, so a
request right after a run finishes reads that run's verdict instead of
starting a second run.
Part of #5387; the restart-loss half waits on #5219.
The collector's oidc/swarm authenticator verifies the bearer offline as a
JWT. The controller's authelia client was registered without
accessTokenSignedResponseAlg, so authelia mints it an opaque
authelia_at_ handle, which that authenticator cannot verify: by config,
no push from the vcs or jobq exporter can be accepted. Register it with
RS256 like every other client whose token lands on an oidc/* receiver.
The SDK logs a failed export at debug only, so a refused push never
reached the journal. AuthenticatedHttpClient now warns on a failed token
mint, a transport error, or a non-2xx answer (status plus a body
snippet), once per kind of failure and again every 10 minutes with a
count, and logs once when a push succeeds after failing.
Refs #5393
Permission::default_for and hive-sh4re's MANAGER_AGENT are gone: every
agent with no permissions key holds Permission::DEFAULT, ruth included.
The agent-name parameters that only fed default_for go with it
(permissions::effective, HiveCopy::before_read/after_read and their
callers).
swarm-controller seeds the grant ruth held by name: at start, a declared
ruth whose permissions are unset gets DEFAULT plus manage_root_agent
written into her key, once, logged. A set key is never touched. It is a
one-shot to delete once every swarm has passed it.
The swarm UI setup assistant opens the create form with
manage_root_agent checked, so a new ruth is created with the grant.
permissions_import no longer adds ruth to the import or splits her
default. Docs and help text no longer say ruth holds the grant by
default; swarmctl-cli.md regenerated.
hh-swarm-queue-connect-failed matched "swarm queue connect failed; this
agent publishes nothing upward", which nothing has logged since the
harness started retrying its first connect and exiting (235bc481b). The
rule now matches the error the harness logs when every attempt has
failed, CONNECT_EXHAUSTED in swarm_queue.rs, and its comment and summary
describe the retry-then-exit behaviour.
A failed MintAgentIdentity node logged nothing: the error reached only
the job graph. mint_identity now warns with the agent and the error, and
hh-agent-identity-mint-failed fires on that line.
Refs #4651
Nothing sets the variable once otel.metricIntervalMs is gone, so the
turn-metrics, jobq-metrics and vcs-metrics exporters export at a fixed
60s, as hive-c0re's container-resource exporter already does. The
observability doc and the removal warning now say so.
swarm-secret-publish and its path unit now run inside authelia's container,
beside the mint, and read each client secret at the path authelia writes it.
The host unit and host path unit, which read the secrets through the
container's root on the host, are gone, and so is
deploy.authelia.hostClientSecretDir, which only they read. The publisher keeps
its store identity, the secret-publisher leaf. The host hands the leaf and the
store's CA into the container as nspawn system credentials, and the unit loads
them; where this host mints the leaf, the container waits for swarm-bao-pki.
Authelia does not wait for the publisher, which retries every 30 seconds while
the store is down.
The container also runs the journal forwarder the other service containers
run, pushing to a receiver of its own on the swarm collector (serviceProducers
swarm-authelia) as a new machine OIDC client (swarm.authelia.otel.clientId)
that authelia registers for itself and the publisher carries to the store. The
fetch unit reads it back under the publisher's leaf, also handed to it as a
system credential, so the swarm-secret-publisher policy gains read on that one
leaf; its stanza keeps create and update, which the exact path would otherwise
take from the services/* prefix. Without the leaf the forwarder is off.
The container drops --link-journal=host, and the host collector stops reading
it. The services' "nothing at <path>" errors name the publisher in authelia's
container.
A swarm-ci-metrics host unit on a 5-minute timer lists every forge
repo's Actions runs as the new read-only ci-metrics account and pushes
each finished run's duration and queue wait over OTLP, stamped at the
run's stop time. The first pass backfills from run 1. swarm-controller
mints the account's token into bao and keeps a ci-metrics team with
read on the Actions unit in every organisation.
Closes#5376
`forgejo-swarm-controller-account`, in the forge's container, now puts
the swarm-controller's forge token into the swarm secret store itself,
under the forge publisher's leaf. The container already receives that
leaf as nspawn system credentials for its journal forwarder; the mint
loads the same three. The token reaches bao on stdin (`value=-`) and
the forge's API through a header file, never as an argument, and never
touches disk.
The mint's guard is a marker holding the published token's name, at
/var/lib/forgejo/swarm-controller-token-name, written last: after the
put, and after every other `swarm-controller-boot-*` token of the
account is revoked through forgejo's admin API, authenticated as the
new token. So a put that failed, or a marker lost while the store kept
an older token, leaves no live token behind once a mint completes. The
mint logs in to the store and checks that the account is a site admin
before it mints, so a store that is down or an account the admin API
would refuse mints nothing on each retry. Failures retry under
store-retry; forgejo does not wait for the mint. No bao read and no
grant change: the forge publisher keeps create/update on the token.
Removed: the host's `swarm-bao-forge-publisher` service and path unit,
which read the token file through the container's root, and their
entries in the store and policy ordering glue. The forge publisher's
policy unit stays; `swarm-bao-forge-oidc` still uses the leaf.
On deploy, an existing forge host has no marker, so the mint runs once:
it mints a new token, publishes it, revokes the old one, and deletes
the old /var/lib/forgejo/swarm-controller-token. The controller's next
forge call gets a 401, re-reads the store and carries on.
The dashboard, its API, the swagger UI and the agent routes move to their
own vhost, `swarm.dashboardDomain` = `<hive>.hives.<swarm.domain>`, under
the swarm domain so authelia's session cookie reaches it. `/`, `/api/` and
`/api/docs/` ask authelia through `auth_request`, and authelia's
`operatorsOnly` rule for each hive's dashboard name lets only the admins
group in. The hive domain keeps matrix discovery and redirects every other
path to the new name.
The name rides as a SAN on the hive's own gateway leaf: the per-hive bao
role allows it, and hive-tls asks for it and checks coverage.
HTTP Basic auth is gone: the htpasswd writer and its test, the
`hivectl gateway` verbs and host-socket requests, the `gateway.auth`
options (now a removed-option error), the unauthorized page and the bcrypt
dependency. tmpfiles deletes a leftover gateway.htpasswd, which held
bcrypt hashes at 0644.
c0re hands out dashboard and agent URLs on the new name
(HYPERHIVE_DASHBOARD_DOMAIN), and the swarm controller's hive roster
carries each hive's dashboard name for the swarm UI's hive list.
Closes#3469