Commit graph

3,830 commits

Author SHA1 Message Date
atlas
9fec1e1dca feat(#3494): provision the agents dashboard from the repo
Grafana served no dashboards: only datasources were provisioned, while
the module header already claimed dashboards were. This ships the agents
dashboard as a file provider and makes that sentence true.

The datasource uid is bound once and substituted into the dashboard at
build time. Committing the literal would make the dashboard a second
speller of a name the datasource already owns, and the drift failure is
silent -- panels render empty rather than erroring.

The shipped copy drops the `DS` datasource variable: it exists so an
operator can pick a store on manual import, and a provisioned dashboard
must not ask.
2026-08-23 22:45:25 +02:00
atlas
a248db1fa2 feat(swarm-authelia-bridge): report a heal as its own outcome
`EnsureAgentIdentity` answered `AlreadyExists` whether it had added the
agent marker to an existing subject or done nothing at all, and the
controller discarded the answer outright. So the one case worth telling
a human about — a subject that was NOT an agent a moment ago — could not
survive the socket, let alone reach a log.

`Healed` is a variant rather than a field on `AlreadyExists` because a
field is ignorable: adding a variant makes every existing match fail to
compile until its author decides what a heal means. That is the property
the old shape lacked.

The store cannot tell a pre-marker agent identity from a human operator
account created without a group — both are `groups: []`. So this is
either the intended migration or an agent joining a person's live SSO
account, and only the caller has the context to tell them apart.

Gated by state/gate-3549-heal.sh (8 arms + mutation): the mutation
collapses Healed back and reddens the discriminating arm while leaving
the anti-noise arm green. The W' control asserts exactly one warn in the
whole run, so a build that warned on every routine ensure would fail.
2026-08-23 19:00:41 +02:00
atlas
d58be6834b fix(#3344): type the roster entry's name as Ident, per review
A bare String in a crate whose whole suite types validated names as
hive_types::Ident, so a consumer had to re-derive at the boundary what every
sibling field gets checked for free.

Ident is strictly narrower than the store's charset — [a-z0-9-] against the
[A-Za-z0-9._-] the users database allows, because that file holds humans too.
Every agent name is a legal Ident by construction (creation parses one before
the job is queued), so the narrowing costs real agents nothing. What it does
mean is that a human hand-added to the agent group cannot be described as an
agent: the reader omits that row and logs it, rather than failing the whole
roster or quietly shrinking the answer.

The REQUEST keeps its String. That side carries what a caller asked for, and
validating it server-side is what lets a bad name be refused with a message
instead of failing to deserialise.
2026-08-20 00:15:48 +02:00
atlas
e71c9cc431 test(swarm-authelia-bridge-sock): pin every response variant's wire shape
The test named for pinning the external tag covered all but the newest
variant, and two doc comments still counted three outcomes where there are
four.

Folded the missing variant in rather than editing the number: a test that
claims to settle the wire shape and quietly omits one is worse than a
narrower one, because the next variant gets added with nothing to point its
author here. The counts are gone rather than corrected — prose that counts
its subject is falsified by every addition and is invisible to a grep for
the vocabulary that changed.
2026-08-20 00:15:48 +02:00
atlas
6a1a349a71 feat(swarm-controller): serve the agent roster
GET /api/agents, beside the POST that creates one. The identity store is
the roster rather than a source to assemble one from, so this is a read
with nothing to merge or reconcile.

It says nothing about health, deliberately. A roster is the set other views
are complete against — it is what makes "this agent has never reported"
expressible, and that only survives while the declared set and the reported
set stay apart.

The two verbs treat a missing bridge differently and the asymmetry is the
design: POST queues a job that fails loud when claimed, GET has nowhere to
defer to and returns 503. Answering [] there would render a store nobody
could read as a swarm with no agents.
2026-08-20 00:15:48 +02:00
atlas
3700167279 feat(swarm-authelia-bridge): mark agent identities with a group, and answer for the set
The users database holds humans and agents in one namespace and nothing in
it said which was which, so a roster read had no predicate to read with.

Marks positively, at creation. The alternative — everyone who is not an
operator — fails in the direction that matters: an account created without
a group is an operator who cannot log in, a mistake the swarm UI docs
already warn about, and it would have rendered as an agent. The group is a
constant for the same reason the operator group it mirrors is one.

An identity that predates the marker gains it when agent creation runs
again, which is already the agreed migration for those; AlreadyExists
therefore reports that the subject was there, not that nothing was written.

ListAgentIdentities reads through this process because the store is owned
by a uid swarm-controller does not have — the same reason the write goes
through here — and answers with names alone, never the digests it sits next
to.
2026-08-20 00:15:48 +02:00
atlas
78217bc10f docs(swarm-authelia-bridge-sock): the bridge reads and writes users.yml, there is no canonical json
The wire crate's module doc and its README both said the bridge "owns both
users.json (canonical) and rendering users.yml internally". That store was
removed — swarm-authelia-bridge/src/store.rs is explicit that users.yml *is*
the store — so both sentences described a file that does not exist, in the
one place a reader goes to learn what the API is for.

The paragraph's actual argument (per-operation rather than
wholesale-replace) is untouched; only the mechanism it cites was wrong.
2026-08-20 00:14:56 +02:00
atlas
dd1156d45c feat(#3549): refuse agent names the swarm already routes on
An agent name was checked for shape and never for meaning, so a name the
system already answers to could be taken. The list is derived, not invented:
operator/manager/root are broker recipients, and forge/reminder/schedule/
system/todo are message senders.

Senders are in it for a reason that is not cosmetic. The harness switches on
the sender name, so an agent called todo would have its messages read as
todo-wakes and one called system as helper events. That is a parse, not a
display quirk.

⚠️ The check is deliberately its own function rather than part of
validate_username, which runs over every user on every write — including
humans this bridge did not create. Folding it in would mean a store already
containing an operator named 'operator' could never be written again: a rule
about what an agent may be NAMED would have become a rule about what the file
may CONTAIN. A test pins that.

This is the static half only. The dynamic half — a name already taken by
another subject — waits on the heal-vs-refuse question open on #3550.
2026-08-19 23:10:02 +02:00
damocles
775ab285b7 swarm-controller: fix stale route reference in snapshot's doc comment 2026-08-19 22:27:12 +02:00
damocles
baac3ef023 swarm-controller: move bulk config-PR read off /api/agents to avoid a name clash 2026-08-19 22:23:45 +02:00
damocles
9067ba52a9 swarm-controller: bulk-read endpoint for agent config-PR status 2026-08-19 21:51:55 +02:00
atlas
1f24193609 feat(swarm-victoriametrics): scrape the store's own prometheus endpoint
The scraper shipped with no targets, so nothing exercised it. This is its
first user, and the one with the least new surface: victoriametrics
publishes prometheus metrics on the listener it already serves queries on,
so there is no exporter, no extra port and no new reach — the collector's
otlphttp exporter already writes to that same loopback address.

Declared from this module rather than the collector's, per the option's own
rule: an entry exists only where the service that named it runs.
2026-08-19 21:40:40 +02:00
atlas
ff5c76c42f feat(#3520): scrape swarm-service prometheus endpoints at the swarm tier
Nothing in this deployment read a Prometheus endpoint, so enabling
/metrics on a managed service added an endpoint and no data. The swarm
collector receives OTLP pushes and does not scrape; VictoriaMetrics
stores what is pushed and has no scrape config. The pipeline was entirely
push-based and every such service is pull-based.

Adds a prometheus receiver, a resource/swarm processor and a
metrics/swarm pipeline alongside the per-hive ones.

The pipeline is separate because that is the ruling, not for tidiness:
every resource/<hive> processor UPSERTS a hive key, so a scraped swarm
sample routed through one would acquire the single label a swarm-level
service must not have. Keeping it out makes the absence structural rather
than something to remember to strip, the same way hive stays a property
of which authenticated receiver accepted a push.

Targets come from an option each service fills in from its own module,
under its own enable, rather than a list assembled here. That is what
puts the scraper and the target on the same host by construction: an
entry exists only where the service that named it runs. Co-location is
true of the all-local deployment and is not a guarantee, and that is
exactly the case where assuming it is invisible.

All three additions MERGE with the per-hive attrsets rather than
replacing them. A plain assignment would drop every hive's receiver,
processor and pipeline and still render a config the collector starts
cleanly on.

Empty target set emits no receiver, no processor and no pipeline — an
enabled scraper with nothing to scrape is the inert configuration this
issue is about, and the target set ships empty here because the targets
themselves are separate issues.

Formatting verified with nix fmt. The evaluation gate is not written yet;
nothing here has been evaluated against a fixture.
2026-08-19 21:40:40 +02:00
damocles
377dbb57e3 swarm-controller: fix broken intra-doc links (private items, rustdoc lint) 2026-08-19 21:12:06 +02:00
damocles
f3e42c93b4 swarm-controller: wire the config-PR webhook, not just the poll 2026-08-19 21:11:35 +02:00
damocles
66c494697f swarm-controller: track agent config-PR status independently of hives 2026-08-19 21:10:59 +02:00
atlas
3c390f8132 docs(#3255): the announce doc described the design the rework replaced
The function's own doc comment still said one subject per hive, four
lines above an inline comment correctly saying one publish for all of
them. Left behind by the single-writer rework.

Swept the rest of the touched files for the old design's vocabulary
rather than fixing only the line argus quoted: every other per-hive
mention is about webhook registration or the callout policy's
extra-subject template, both of which are still per-hive and still
accurate.
2026-08-19 21:05:52 +02:00
atlas
33958d78ae refactor(#3255): a constant, not a module with an essay
Review call: 46 lines of documentation around a single constant, part of
it already stale. The worst paragraph explained why the earlier per-hive
shape had been justified wrongly — history of a design that never
shipped, written into the file within an hour of that design being
dropped. A file is not a changelog; why it was wrong belongs in the PR.

The constant moves to lib.rs beside the status bucket name, keeping only
the rationale that stays true: three crates must agree on the string, and
the one that agrees hardest speaks neither jetstream nor kv, which is why
it cannot sit behind a feature gate.

status earns a module of its own because it holds a bucket name AND the
functions that open it. This held a constant.
2026-08-19 21:05:52 +02:00
atlas
9b939f4626 refactor(#3255): one knowledge subject, single writer and many readers
Review call: the event was addressed per hive — `$SWARM.events.<hive>.knowledge`,
published in a loop over the roster, granted through a wildcard. It does not
need to be. The payload is empty and the event means the same thing to every
hive, so one publish to one subject delivers exactly what N publishes to N
subjects did, and core NATS already fans out to whoever is subscribed. A hive
that was down misses it either way and reconciles on its next periodic pull.

That deletes rather than reshuffles: the roster loop, the wildcard, and the
shared subject-building function whose entire purpose was keeping the grant and
the publish from drifting apart. With one literal there is nothing to disagree
about.

The per-hive shape was justified by the callout policy's rule that an extra
subject must contain the hive name. That rule governs `extra_hive_subjects` —
what a HIVE may publish. This subject lives in the controller's reader grant,
which the rule does not constrain, so a real rule was carried across into a
decision it had no authority over.

Knowledge becomes its own category rather than a leaf under a general event
namespace, since a namespace shaped for events that do not exist yet is a
decision made before there is anything to decide from. The empty config-PR match
arm goes with it: an arm with no body claims this is where the deploy path is
handled, and it is not.

The deny test stays and matters more, not less: with one shared subject a forged
event would reach the whole swarm where a per-hive one reached a single hive.
2026-08-19 21:05:52 +02:00
atlas
84125b0806 docs(#3255): correct the reaper invariant the swarm webhook test guards
The test's doc named knowledge::ensure_webhook as one of two hive-side
reapers. That function is gone; its replacement matches the full URL and
cannot touch another hive's hook.

The assertion arm stays. The hazard is not what this repository's source
says, it is what is deployed — a hive still running the previous version
reaps by suffix until it is upgraded, so the arm guards the transition
rather than a current code path. Recorded when to drop it.
2026-08-19 21:05:52 +02:00
atlas
d2a550e685 feat(#3255): hives stop owning the knowledge webhook, and clean up their own
A webhook has exactly one target URL, so every hive registering one
against the shared internal/knowledge repository was last-writer-wins
rather than idempotent: all but the most recent silently stopped
receiving deliveries. The swarm controller holds the single registration
and now addresses an event to each hive over the queue instead.

This is a migration, not a deletion. Not registering any more fixes
nothing on a hive that has already run — the hook it created persists on
the forge, so the contention would survive on exactly the deployments
that have it while fresh installs looked fixed. The hive that created a
hook removes it.

It removes only its OWN, matched on the full URL rather than the
/webhook/knowledge suffix. A hook with that suffix and a different base
belongs to another hive, possibly one not yet upgraded, and deleting it
would break that hive's knowledge sync until it caught up. Reaping a
neighbour's registration is the behaviour being removed here; doing it
while fixing it would only invert the direction.

The predecessor did reap by suffix, to clear loopback hooks left by an
older single-hive layout. That was safe when a hive was alone on its
forge and is not safe now. The hive-side registrars also acted as reapers
of hooks under their own path, which is why the swarm hook lives under
/webhook/forge/; removing this registrar removes that reaper too.
Intended, and stated because no reviewer would infer it from the diff.

The receive endpoint goes with it. A live HMAC-verified
/webhook/knowledge that nothing can legitimately reach would tell the
next reader that this is how a hive learns about knowledge changes.

Docs move in the same commit: docs/swarm/README.md said two hooks exist
per swarm-wide repo and neither should be deleted, which is now true for
agent-configs and wrong for internal/knowledge — a half-correct
description being worse than an uncorrected one.
2026-08-19 21:05:52 +02:00
atlas
89050ef34b feat(#3255): hives subscribe to their own knowledge event
A hive learned the knowledge repository had changed only by registering
its own forge webhook. This subscribes to the per-hive subject the
controller now publishes on and calls the pull this daemon already runs
at boot.

Shares the hive's ONE queue connection rather than opening a second: a
second connect would double the auth-callout traffic against authelia and
give the two paths independent reconnect state, so one could be serving
while the other was still down. Same argument as the controller side.

No payload is read, because there is none to read — the webhook handler
this replaces took two fields from Forgejo and used neither, then ran
`git pull`, which re-derives everything from the repository.

At-most-once, and that is not a regression: a webhook delivery to a hive
that is down is lost identically today, and the boot pull covers it.
JetStream would require this end to publish to
`$JS.API.CONSUMER.CREATE.<stream>`, which the callout policy does not
grant, so durability would cost grants on both sides to remove a failure
the boot pull already handles.

⚠️ Documented at the call site rather than left implicit: a refused
subscription is indistinguishable from a quiet one, because NATS reports
authorization violations asynchronously on the connection. If hives stop
hearing events, the server log is the thing that knows.

futures-util comes from the workspace (same version swarm-controller
already uses), not a new dependency version.
2026-08-19 21:05:52 +02:00
atlas
bac4a8b6a1 feat(#3255): announce a knowledge change to every hive on the queue
The controller verified a knowledge delivery, logged it, and returned OK.
Nothing downstream ever heard about it, so a hive learned the repository
had changed only by registering its own webhook — which is the
last-writer-wins contention this issue is about.

The event carries no payload. The hive-side handler this replaces reads
two fields from Forgejo's push webhook and uses neither — both are
filters — then runs `git pull`, which re-derives everything from the
repository. What crosses the queue is an edge trigger, and fields would
invent a contract nobody reads.

One subject per hive, so the callout policy can express "this hive may
hear its own events" at all; a subject with no hive component is the same
subject for every hive.

`ConfigPr` deliveries are deliberately not forwarded. A hive does not
want to hear that a config PR opened — it wants to be told when to
rebuild from main, which the controller decides after a merge rather than
by relaying this delivery. That is deploy coordination's job, and the
empty arm is there so the omission reads as scoped rather than forgotten.

Fails soft: a missed announcement costs a hive stale knowledge until its
next boot pull, which is the same cost as a webhook delivery to a hive
that was down — what this replaces. A permission failure cannot be
observed at the call site (a NATS authorization violation is reported
asynchronously on the connection, reaching a client as a timeout or as
nothing), so the doc says the flush proves only that the bytes left this
process and points at the server log.
2026-08-19 21:05:52 +02:00
atlas
8baf1899d8 feat(#3255): name the swarm event subjects, and let the controller publish them
The controller could not emit an event at all: a reader's grant is
`reader_subjects()`, which is `$JS.API.*` only, so a publish to any event
subject would be refused — and a NATS refusal reaches the client as a
timeout, so the visible symptom would have been a hive that never hears
about a change, with nothing in any log naming a permission.

Adds `swarm_queue_client::events`, following `status::BUCKET`: three
crates must agree on these strings (the controller publishes, a hive
subscribes, the callout responder decides whether the publish is
permitted), and a literal repeated across crates is an agreement nothing
checks. The responder speaks neither jetstream nor kv, so the module is
unconditional and carries no NATS types, exactly as the bucket name is.

The grant takes the wildcard form from the same function the publisher
calls, so the two cannot drift; a separate wildcard constant would have
re-created the disagreement this module exists to prevent.

Tests pin that a reader gets the subject and that a hive does NOT — a
hive able to publish here could tell a neighbour the knowledge repo
changed when it had not, which is an unauthenticated write into someone
else's control path. That one asserts on the subject root rather than a
rendered subject, so a future event leaf fails it too instead of passing
because the test only knew about `knowledge`.

Both assertions mutation-tested: removing the grant fails the reader
test, granting a hive the subject fails the denial test, each on its own
assertion line, and the unmutated tree is green.
2026-08-19 21:05:52 +02:00
atlas
893b686c15 fix(#3527): a missing source must report as zero, not as empty
argus, reviewing, ran the guard against a source path that does not
exist rather than one that is empty. grep writes nothing to stdout in
that case, so `|| true` left the count variable empty and the -eq test
died with "integer expected" instead of reporting.

The unit still failed — cat hits the same missing file and set -e stops
it — but with a generic "no such file" rather than the message naming
which half is absent, which is the only thing this guard is for.

`|| echo 0` on all three counts. The gate gained the arm that was
missing: an absent source, asserted to fail THROUGH the guard rather
than merely to fail.
2026-08-19 20:27:00 +02:00
atlas
40557ffb74 fix(#3527): count the hive half, not the assembled bundle
The guard inspected the assembled file for any certificate. The system
store always holds certificates, so it passed unconditionally — including
in the one case it was written to catch, where the hive CA half
contributed nothing.

That half is the only one that matters here: every name these consumers
verify is issued by our own CA, so a bundle of nothing but public CAs is,
for this purpose, an empty bundle that measures as full. The failure is
silent and total — the unit reports success and every egress TLS call to
a swarm service then fails.

Counts the source on its own before assembling, and checks the result
carries what both halves brought, so a source truncated between the count
and the copy is caught too.

Scope is stated at the guard: it proves the anchor was contributed, not
that it is usable. A consumer reading only the first certificate ignores
it regardless, which is what took the swarm collector down, and no check
on this file can see that. Only a handshake can.
2026-08-19 20:16:16 +02:00
iris
149f745e76 docs(matrix): trim upgrade notes per review
- serverName upgrade note: state that it changed, drop the historical
  list of what it changed from/to
- openFirewall upgrade note: remove entirely, change is old enough
  that it's no longer worth flagging
2026-08-19 20:01:45 +02:00
iris
47f5e57c68 docs(matrix): demote breaking-change notes to clearly-scoped upgrade sections
serverName/gatewayHost and openFirewall both led with 'Breaking
change'/'has now moved twice' phrasing woven into the primary
explanation, reading as an active/ongoing concern rather than
settled current behaviour with a historical upgrade note attached.
Reframed to state the current default first, then a demarcated
'Upgrading...' section/note for deployments carrying an older
value forward — same pattern swarm/README.md's own upgrade notes
already use. No factual content changed, removed, or added.
2026-08-19 19:55:21 +02:00
iris
6d9a2fa9f6 swarm-ui: fix underline not showing on the initial page load
Root cause (found via a zoomed pixel-level screenshot check, not
guessed): the indicator span always exists in the DOM starting from
left:0/width:0/transparent, and the very first real position landed
via a CSS-transitioned change from that fallback rather than a snap —
so the underline visibly grew in from nothing over 140ms instead of
being there immediately, reading as entirely absent on a fast/slow
first paint alike depending on timing. Every subsequent navigation was
unaffected (always transitioning between two already-visible states).

Fixed with a one-render-only transition suppression
(indicatorSettledOnce, gated one tick behind the indicator's first
non-null commit) so the first placement snaps instead of animating in,
while every later hop still animates normally. Verified against the
exact repro: a fresh page load at the same short virtual-time-budget
that previously showed no underline now shows it immediately.
2026-08-19 19:54:52 +02:00
iris
daabb4fe6a swarm-ui: brand text follows the nav accent too
The brand/logo text now rides the same accent value the nav underline
uses, including mid-sweep, so the header reads as one accent changing
rather than the underline alone — per mara's review comment.
2026-08-19 19:54:52 +02:00
iris
a5584d0fcd swarm-ui: plain fade + multi-hop underline sweep, per review
Content entrance was a scale+overshoot pop; mara didn't like it on
review, swapped for a plain fade. The nav underline now hops through
every nav item it passes over on its way to the new active one
(position and colour together) instead of tweening directly between
the two endpoints, matching what she actually meant by "move through
color wheel" — a->c visibly touches b's colour along the way, not a
smooth continuous hue rotation.
2026-08-19 19:54:52 +02:00
iris
6be1bbd4f7 swarm-ui: page-switch animation (pop entrance + nav underline travel)
Shell's page content now remounts on every navigation and plays a
scale+fade pop-in entrance. The active nav link's underline is now a
single shared element that slides to its new position instead of
snapping, re-colouring to a per-tab accent — a discrete cycle through
the existing base16 chromatic slots, not a continuous hue rotation.
Both are gated on the data-motion/prefers-reduced-motion plumbing
lib/motion-apply.ts already had wired and waiting for a first real
consumer.
2026-08-19 19:54:52 +02:00
iris
213e0a1b89 swarm-ui: jobs page auto-refresh
Reuses HivesPage's exact RefreshIntervalPicker/useRefreshInterval
pattern (30s default, off/10s/30s/1m presets, pauses while the tab is
backgrounded). Ticks a refreshToken bump rather than doing its own
fetch — JobqGraph/JobqRollup already accept that prop as their
documented refetch lever, from the dashboard's rebuild-queue view.
2026-08-19 19:33:20 +02:00
iris
3b0180e7d6 docs: trim observability.md's duplicate option-reference sections
The hand-maintained per-option reference (services.hyperhive.otel.*)
restated what nixosOptionsDoc already generates from the same nix
module's mkOption descriptions into /options/ — two copies of the same
fact, one of which the build keeps honest and the other doesn't.
Trimmed to a pointer, keeping only the architecture/security prose a
flat per-option listing can't express. The port-conflict cross-
reference (hive tier 4318 vs swarm tier 4319) that isn't in either
option's own description is already covered in swarm/services.md,
which this doc already links to.
2026-08-19 19:30:39 +02:00
atlas
36810ae598 fix(#3529): create agent config repos in agent-configs, not agents
WIP — compiles per an earlier build, but the verifying build/test run was
cut short by a graceful stop. Re-run gate.sh before pushing.

The controller created every agent config repo, its collaborator entry,
its branch protection and its seeded agent.nix/flake.nix in the agents
org. Config repos live in agent-configs, which is where hive-c0re
reconciles, merges and mirrors them — so a repo created in agents is
invisible to all of those, and nothing errors, because both orgs exist
and both accept a repo.

Root cause was a doc comment asserting something false: AGENTS_ORG
claimed to be the same org hive-c0re uses for its config-repo path. It is
not — hive-c0re's agents org is the namespace repos an AGENT ASKS FOR
land in, and its config repos use agent-configs. The whole flow inherited
the wrong premise from that sentence.

The merge gate survives the move: hive-c0re provisions the operators team
in both orgs, with a comment recording that missing the agent-configs
copy once left every config repo unprotected.
2026-08-19 19:06:46 +02:00
damocles
c5bea0b8c4 forge-notify: widen earlier-activity marker to the full timeline 2026-08-19 19:06:16 +02:00
damocles
530eedd5f4 forge_notify: flag earlier comments the latest_comment_url blurb hides 2026-08-19 19:06:16 +02:00
atlas
b8b571fea0 fix(#3524): stop naming the trust bundle as the oidc issuer anchor
The swarm collector could never verify its OIDC issuer, so it exited at
startup on every boot and the hive tier dropped every metric.

`issuer_ca_path` loads only the FIRST certificate in the file it names.
The bundle assembled for this container is `system CAs ++ hive anchors`,
so the swarm CA sits ~123rd and was never in the pool: the extension got
whichever public CA sorts first, could verify nothing of ours, and failed
`x509: certificate signed by unknown authority` — with 125 valid
certificates in the file.

Leaving the option unset makes the extension use the process trust store,
which `trustBundle` already populates via `SSL_CERT_FILE`, and that
consumer reads every certificate regardless of order. One file, two
consumers, opposite parsing; the fix is to stop naming it twice rather
than to reorder the bundle.

Measured with the deployed binary against the deployed config, varying
only the CA source: root-only OK, root-first OK, root-last FAILS,
root-second FAILS, and unset-with-SSL_CERT_FILE OK against a control that
correctly fails when the anchor is absent.
2026-08-19 18:23:42 +02:00
damocles
f5ff8698f3 swarm-controller: extract jobq_metrics into its own hive-jobq-metrics crate 2026-08-19 18:10:22 +02:00
damocles
621245306f swarm-controller: move OTEL deps to workspace level, drop jobq_metrics singleton 2026-08-19 18:10:22 +02:00
damocles
1f5a7b71ed swarm-controller: export jobq state rollup as OTEL gauges 2026-08-19 18:10:22 +02:00
iris
b25aa157df swarm-ui: give Panel an optional header icon slot
Adds an `icon` prop to Panel (small emoji glyph left of the title,
aria-hidden, chosen per panel with no default) and wires it into every
current Panel caller: hives (bee), create-agent's form (robot) and info
panel (identity card, moved off the info panel's body copy where it
started as a one-off), jobs (puzzle piece), the components gallery
itself (toolbox), and the 404 page (compass). Adds a components-page
demo section and a whimsy-section pointer in the design guide.

Closes: #3508
2026-08-19 17:22:48 +02:00
damocles
582b5cf83d fix(#3500): make the swarm-controller forge account a site admin 2026-08-19 17:13:54 +02:00
damocles
aca39072d5 fix(#3072): stop waking every agent on a re-applied m.space.child state event 2026-08-19 17:12:01 +02:00
iris
1207a0e282 swarm-ui: create-agent layout fills the page width, centered
mara: the two cards were neither centered nor filling the space in
two-column mode. Dropped the page's own max-width entirely — Panel
already has no width opinion, and once both cards are equal-width
flex children there's no reason for an extra cap between them and
.shell-body's own 60em/centered column. Removed the now-pointless
wrapper div along with it.

Checked centered + filling behaviour at both a normal (1200px) and an
ultra-wide (1600px) viewport, and narrow-viewport stacking still
works.
2026-08-19 17:11:59 +02:00
iris
3b339b2232 swarm-ui: equal-width create-agent cards, full-width inputs
mara's follow-up: both cards should be the same size (flex: 1 1 0
instead of the form keeping its own narrower basis), and the form's
inputs should fill the card rather than stopping at their old 16em
cap. Dropped that cap from the shared form kit (.ui-form-control /
.ui-form-field) entirely rather than overriding it per-page — the kit
itself has no width opinion now, same as Panel already has none; a
page that wants a narrower field caps its layout, not the kit.

Checked ComponentsPage's standalone samples (no regression, just
wider) and narrow-viewport stacking (still moves the explanation
under the form).
2026-08-19 17:11:59 +02:00
iris
f30897b44e swarm-ui: create-agent info panel reads as the finished flow
mara's review: the info-panel copy should describe the intended end
state (a running agent on the chosen hive), not narrate today's
partial implementation (deploy isn't wired up server-side yet — noted
in this file's existing top comment for maintainers, kept separate
from the user-facing copy).
2026-08-19 17:11:59 +02:00
iris
fc65811d9f swarm-ui: fill create-agent's empty column with an explanatory panel
mara's follow-up on the field-alignment fix: narrowing the form panel
left a lot of bare space next to it, and asked for something that
fills it while helping a new user understand what the page does.

Adds a second panel beside the form (stacks under it on a narrow
viewport) explaining the job chain '/api/agents' actually queues:
an authelia identity, then a forge config repo — no container exists
yet after this page, and deploying one onto the chosen hive is a
separate step that isn't wired up server-side.
2026-08-19 17:11:59 +02:00
iris
f14fc154cc swarm-ui: fix create-agent form field alignment and panel width
TextField and SelectField's shared FormField wrapper had no width of
its own, so inside the form's shrink-to-fit flex column each field's
input/select resolved its 'width: 100%' against its own shrunk
wrapper rather than a shared column width — two fields with
differently-long labels ended up with differently-wide controls.
FormField now caps its own width the same way the control already
does, so every field in a form lines up regardless of label length.

Also wrapped the page in a max-width container: Panel has no width
opinion of its own, so it filled the full page column, leaving a lot
of bare panel to the right of the ~16em-wide form.
2026-08-19 17:11:59 +02:00
atlas
7880483b51 swarm-otel: stamp the swarm on every hive's pipeline
mara on the tracking issue: "we need the swarm label for upstream otel at
least (the out of swarm one)."

Stamped in the per-hive `resource` processor rather than on a separate
upstream-only pipeline, which would double the pipeline count to withhold one
constant label from the local store. It is redundant there — one metrics store
per swarm, so every series in it already belongs to this swarm — but a constant
label multiplies no series, and it means what leaves and what stays have the
same shape.

Upstream is where it stops being redundant: that is the one hop where several
swarms can land in one store, and samples that cannot name their swarm collide
there exactly as hives collided here before per-hive receivers existed.

`unknown` when unnamed rather than an absent label, copying the agent path so
a query never has to handle both "the label is missing" and "the label says
unknown".
2026-08-19 17:06:47 +02:00