Commit graph

3,829 commits

Author SHA1 Message Date
atlas
072dbd80a7 fix(#3343): move the all-local queue derivations into the deployment mode
Review point: the co-located defaults are an auto-deploy special case and
belong with the other ones, not inside each option's own default. An
option should describe itself; the mode describes what a deployment shape
implies. local-defaults.nix already says exactly this in its header.

Options now default to empty (= unset, which the assertions refuse), and
enableAllLocalDefaults fills in loopback + the minted secret path.
2026-08-16 19:37:49 +02:00
atlas
92025e01de fix(#3343): let the swarm controller reach a queue it is not co-located with
The controller's queue coordinates were gated on `autheliaCfg.enable &&
natsCfg.enable` -- i.e. on this host running both. Split the hosts and
the whole env block vanished, which the daemon's own rule reads as "no
queue configured": it started cleanly, served /api/hives, and silently
never connected or published.

Make the coordinates options instead, defaulted to the co-located values
so nobody types a path they didn't need, and assert each one so a split
deployment fails at eval naming the option rather than running blind.
The queue is required for a controller -- /api/hives/status reads the KV
-- but co-location with it is not.
2026-08-16 19:37:49 +02:00
damocles
4122960530 swarm-controller: fix forgeTokenFile default referencing a nonexistent forge.enable 2026-08-16 19:24:49 +02:00
damocles
c1e46b378d swarm-controller: provision + collect its own forge account token 2026-08-16 19:17:34 +02:00
iris
b03ae55786 swarm-ui: use native Intl.RelativeTimeFormat for fmtAgo, not hand-rolled
Answers mara's question on PR#3342 (lightweight dep for fmtAgo?): no
dep needed, Intl.RelativeTimeFormat is built into the runtime and its
narrow style produces the same '5m ago' shape, verified with a real
call rather than assumed from the spec.
2026-08-16 19:05:01 +02:00
iris
9786a9a519 swarm-ui: extract overview into its own page component
Per mara's review on PR#3342 — the hive-status view shouldn't live
inline in App.tsx. Moves it to pages/OverviewPage.tsx, mirroring
JobsPage's shape: App.tsx stays routing-only, each page owns its own
fetch + render.
2026-08-16 19:05:01 +02:00
iris
2460d7fec7 swarm-ui: wire hive overview to the real status aggregate
App.tsx now fetches GET /api/hives/status (the swarm-controller
aggregate: one row per roster hive, freshness derived at read time
from the status bucket) instead of GET /api/hives + a static
'configured' chip. Renders fresh/stale/never_reported/unknown as
StatusChip tones with a relative age, per the placeholder comment that
was already waiting on this endpoint to exist.

Adds a small local fmtAgo helper (src/util.ts) mirroring the
dashboard package's near-identical formatter — not worth sharing
across a vanilla-JS and a Preact/TS call site.
2026-08-16 19:05:01 +02:00
damocles
4f8b78a4d4 hive-forge dependency: don't drop the original errors if the verification read-back also fails 2026-08-16 18:51:46 +02:00
damocles
eefd971bd8 hive-forge dependency: surface a converged-despite-error case instead of swallowing it 2026-08-16 18:48:39 +02:00
damocles
7668454bae hive-forge: verify dependency add/remove by read-back instead of trusting HTTP status 2026-08-16 18:47:01 +02:00
atlas
3e5587dd4d fix(#3150): let an SSO login adopt the matrix account it names
An SSO login whose derived localpart already exists silently gets a
random one instead: the candidate must be *available*, an existing
unlinked account is not, and `unique_id_fallbacks` defaults to true.
The operator's own login minted a stray account this way.

Fill the three identity_provider fields that decide which account a
login lands on: `trusted` (upstream's precondition -- self-hosted and
fully controlled -- is structural here, the issuer is always the swarm's
own authelia), `userid_claims` pinned to preferred_username so the
default ladder cannot fall through to an email local part, and
`unique_id_fallbacks = false` so a collision errors instead of
succeeding as the wrong user.
2026-08-16 18:28:57 +02:00
damocles
5e7f74e92d reword ack->mark-done terminology on mark_todos_done and get_loose_ends 2026-08-16 17:55:33 +02:00
damocles
6f208c7301 get_loose_ends: clarify that an in-flight todo (e.g. a running bash task) doesn't need an early ack 2026-08-16 17:55:33 +02:00
atlas
8c51e37804 swarm-authelia: let a client declare its token-endpoint auth method
tuwunel authenticates at the token endpoint with the secret in the POST
body. Authelia enforces the method a client is REGISTERED with rather
than accepting whichever one arrives, and its default is
client_secret_basic — so the matrix login completed, consent was
granted, and the very last hop failed:

  Client authentication failed ... The request was determined to be
  using token_endpoint_auth_method client_secret_post, however the
  OAuth 2.0 client registration does not allow this method.

The failure names neither the secret nor the redirect, and it lands
three layers from its cause, which is why it read as a credential
problem.

Adds a per-client tokenEndpointAuthMethod, null by default so every
existing client keeps authelia default (forgejo authenticates with
basic and is unaffected), and sets client_secret_post on the matrix
client only.
2026-08-16 17:48:52 +02:00
iris
b4b4967ec9 address review: trim JobqGraph.tsx's comment under 30 lines, fix stale .jsx ref
Both introduced by this PR's own JSX->TSX conversion, caught by argus:
the top-of-file comment grew to 42 lines converting it, and JobsPage.css
still pointed at 'JobqGraph.jsx's own comment' after the rename.
2026-08-16 17:30:06 +02:00
iris
527ed8f3e2 swarm-ui: add a jobs tab, and type JobqGraph as real TypeScript
New /jobs route in swarm-ui, reusing the shared JobqGraph component
against swarm-controller's own GET /api/jobq/graph (same wire shape
hive-c0re's dashboard already consumes, different endpoint, no fork).

Converted JobqGraph.jsx to JobqGraph.tsx with real prop/wire types
(mirrors hive_jobq_wire's GraphNode/GraphDep/State by hand) instead of
a hand-maintained ambient .d.ts at the swarm-ui consumer side — the
.d.ts would duplicate the prop list and drift from the source the
moment the component's signature changes without the declaration being
touched. Both dashboard (untyped consumer, esbuild strips types) and
swarm-ui (tsc --noEmit) build/typecheck clean off the one file.
2026-08-16 17:30:06 +02:00
iris
61977514c6 swarm-ui: serve the hyperhive favicon
Same pattern frontend.nix already uses for the dashboard: the SVG
lives outside the npm tree at branding/hyperhive.svg, copied into
$out during the nix install phase (not build.mjs, so plain npm build
consumers still work, same split frontend.nix uses). Added the
matching <link rel="icon"> to index.html.
2026-08-16 17:23:02 +02:00
damocles
bc40947550 job_queue: trim graph_snapshot's doc comment back under the 30-line comment-block cap 2026-08-16 17:19:04 +02:00
damocles
eae04ac2c5 address review: switch hive-c0re to hive-jobq-wire's shared parse_states/filter_nodes_by_state, note the generic shape in endpoint docs 2026-08-16 16:59:54 +02:00
damocles
08efd7875e address review: move parse_states/filter_nodes_by_state to hive-jobq-wire, rename placeholder enums, trim core-mirroring framing 2026-08-16 16:59:54 +02:00
damocles
962b7e60f8 swarm-controller: wire the swarm-level job graph, no nodes yet 2026-08-16 16:59:54 +02:00
atlas
4114d6898b docs(swarm): state the secrets rule precisely instead of carving an exception
Review: the public keys are not an exception, the paragraph was
incomplete. It is private key material and access tokens that are
file-based so they stay out of the nix store; public material in a
public store is fine.

Fixes the rule itself, which removes the need for the special case the
previous commit added.
2026-08-16 16:21:46 +02:00
atlas
7c3154e3f4 swarm-nats: cut the comments back to what the code cannot say
Review: high comment-to-code ratio. Measured — 197 of 337 added lines
were comments, and net of the block that only moved it was still +135
comment against +117 code.

Kept the constraints, the rejected alternatives and the costs; dropped
everything that restated what the code plainly does, and the
slash-stripping explanation that was duplicated in the generator.
2026-08-16 16:19:30 +02:00
atlas
22a191519f docs(swarm): the queue auth-callout nkeys, and why two of its keys are values
secrets.md is the page that answers "who mints this and where does it
go" per topology, and the callout nkeys had no row. Adds one, plus the
`nk` invocations an operator needs on every topology that is not
all-local.

The note exists because the change provokes a question the page would
otherwise answer wrongly: this page opens with "a secret is a path,
never a value", and the queue takes two public keys as values. A reader
who meets those after reading that line concludes the page is stale.
2026-08-16 16:07:36 +02:00
atlas
4365520ada feat(swarm-nats): mint the auth-callout nkeys on all-local hives
A hive whose whole swarm is one box had to be handed two nkeys by hand
before its queue could authenticate anyone, which is the one deployment
shape where nobody else can supply them.

`autoGenerateCallout` mints both keypairs on the host on first boot,
keeps the seeds at 0600 host-side, and writes only the public halves
into a fragment the server reads at start. The all-local mode turns it
on; everywhere else the options stay operator-supplied and the
fail-closed eval assertions keep their full force.

The server config is not rewritten to do this. A wrapper includes
upstream's rendered `settings` verbatim plus the runtime fragment, and
the fragment wins — measured, along with the property that makes the
whole shape safe: the empty strings the options render in auto mode are
values `nats-server` refuses to start on, so any field the merge fails
to reach fails closed loudly rather than leaving a walk-in-able server.

The wrapper, the settings symlink and the fragment are siblings in one
runtime directory, and that is forced rather than tidy: NATS resolves an
include with filepath.Join against the config file's own directory,
which strips a leading slash, so an absolute include silently becomes a
relative one and the server never finds it. The includes are therefore
bare filenames. That also means nothing in the closure would otherwise
name the rendered settings, so the generator's symlink to it is what
keeps it from being garbage-collected under a running server.

`accounts` and `authorization` are defined once and rendered twice, into
`settings` and into the fragment template. Written out separately they
would diverge silently and backwards: the fragment is the later
definition, so a future edit to `settings` alone would be ignored on
exactly the hives that use auto mode.

`validateConfig` goes off in auto mode because `nats-server -t` rejects
the empty keys at build time; the parse check moves to server start,
where the fragment exists. Upstream's own option description names this
case.
2026-08-16 15:59:06 +02:00
iris
c88ba0d827 nix: stop restating the npm-deps-hash regenerate command in 3 places
frontend.nix stays the one authoritative explanation; swarm-ui.nix and
checks.nix now just point at it instead of each restating the
prefetch-npm-deps command in their own words.
2026-08-16 15:57:22 +02:00
iris
2a67cc0e69 nix: single-source frontend npmDepsHash into frontend/npm-deps-hash
The hash was hardcoded independently in nix/packages/frontend.nix,
nix/packages/swarm-ui.nix, and nix/checks.nix's inline
swarm-ui-typecheck derivation, all three building from the one
frontend/package-lock.json. Nothing enforced the three copies staying
in sync, and on a recent PR only some of them got updated when the
lockfile changed.

Moved the hash into a new file, frontend/npm-deps-hash (plain text, no
trailing newline, co-located with package-lock.json so it reads as
the lockfile's other half), and all three derivations now
builtins.readFile it instead of hardcoding their own copy. A lockfile
change now only needs prefetch-npm-deps + one file overwrite; the
other two derivations pick it up automatically.

Verified: nix eval against all three derivations' npmDepsHash
attribute (eval-only, not a build) confirms all three resolve to the
same value read from the one file.
2026-08-16 15:57:22 +02:00
iris
85ef5e5fbe hive-agent: show mark_todos_done ids in the terminal, not just a count
fmt_args_generic's generic array handling collapsed `ids: [4]` — the
count — since mark_todos_done had no dedicated match arm. Added one,
matching the file's existing per-tool pattern (extracted into its own
helper to stay under the 100-line clippy limit on fmt_hyperhive_tool).
2026-08-16 15:45:05 +02:00
iris
92bb5340b7 nix: bump npmDepsHash for the new preact dependency
frontend/package-lock.json changed (preact added to dashboard + shared
in the earlier commits on this branch) but the fixed-output npm-deps
hash pinned in nix/checks.nix, nix/packages/frontend.nix, and
nix/packages/swarm-ui.nix (all three hash the one shared lockfile,
per checks.nix's own comment) wasn't updated to match — argus caught
it on review. Recomputed with nix run nixpkgs#prefetch-npm-deps.
2026-08-16 15:18:25 +02:00
iris
e14887164b jobq-graph: author JobqGraph in real JSX, not hand-written h() calls
Mara on PR#3315: "shouldnt the pattern be that the old dashboard has a
dep on preact and has a preact instance running for the jobq view?
then we could get rid of a lot of extra plumbing" - right: the plain
h() authoring existed only to dodge adding JSX support to the
dashboard's esbuild config, and that dodge is exactly the plumbing to
remove now that the dashboard already depends on preact.

- JobqGraph.js -> JobqGraph.jsx, rewritten in real JSX.
- dashboard/build.mjs: added jsx: 'automatic', jsxImportSource: 'preact'
  to the JS-bundle esbuild call (esbuild already picks the jsx loader
  for .jsx by extension; this just sets the transform mode, matching
  swarm-ui's config). No other entry in that bundle uses JSX today.
- shared/package.json: export target updated to the .jsx file.

The CSS-as-page-level-@import structure is unchanged and stays that
way regardless of JSX: dashboard bundles this component transitively
through one esbuild call whose .css loader is 'text' (for the
shadow-DOM components that need their CSS as a literal string), and
esbuild's loader map is global per call, not per-module - importing
CSS from this component would silently pick up that loader too.
Explained in the file's own top comment.

Verified: npm run build (whole workspace) and npm run typecheck
(swarm-ui) clean. Re-ran the same headless-chromium screenshot against
a mock GET /api/jobq/graph payload as the previous verification -
pixel-identical to the h()-based version, confirming this is a pure
authoring-style refactor with no behavior change.
2026-08-16 15:18:25 +02:00
iris
ecf01f5e87 docs: remove historic framing, state what JobqGraph is now 2026-08-16 15:18:25 +02:00
iris
37161cd136 dashboard: replace <hive-jobq-graph> with a shared Preact component
Ports the shadow-DOM <hive-jobq-graph> custom element
(frontend/packages/shared/src/jobq-graph/) to a Preact component
(JobqGraph.js) shared by the dashboard and swarm-ui, per hyperhive#3310.

- JobqGraph.js: written with plain h() calls (no JSX) so the same file
  compiles unmodified under both the dashboard's text-loader CSS config
  and swarm-ui's JSX config. Exports `JobqGraph` for JSX use and
  `mountJobqGraph(container, props)` for the dashboard's non-JSX
  imperative mount, returning a `{refresh(), update()}` handle matching
  the old custom element's public surface. Same rendering contract as
  before: indented state tree, payload.label verbatim, payload.data as
  a generic key/value list, "waits on: <label>" text for Node-kind deps,
  per-state filter checkboxes, optional cancel button.
- jobq-graph.css: light-DOM adaptation of the old shadow-scoped
  stylesheet (:host -> .jg-root, otherwise unchanged).
- dashboard/src/builds.js: local mountJobqGraph() renamed to
  mountRebuildQueue() to avoid colliding with the newly-imported shared
  mountJobqGraph; cancel handling is now a plain onCancel callback
  instead of a DOM CustomEvent listener (no shadow boundary to cross
  anymore).
- dashboard + shared package.json: added preact as a dependency (matches
  swarm-ui's existing pin, 10.29.8) - the dashboard was a vanilla-JS MPA
  with no Preact/JSX pipeline before this.
- Removed the old hive-jobq-graph.js/.css entirely (confirmed via grep
  it had exactly one consumer, dashboard/src/builds.js, so this is a
  clean swap, not parallel maintenance of two implementations).
- Updated stale doc-comment references to the old element name in
  builds.html, tabs.js, swarm.js, docs/web-ui/dashboard.md, and
  hive-c0re/src/job_queue/mod.rs.

Verified: npm run build (whole frontend workspace) and npm run
typecheck (swarm-ui) both clean; cargo build/clippy/test -p hive-c0re
all clean (331 tests, 0 failures); headless-chromium screenshot of
/builds.html against a mock GET /api/jobq/graph payload confirms full
visual/behavioral parity with the old custom element (tree, filter
checkboxes, cancel buttons, error text, waits-on line, data list, live
build log panel).

This covers the dashboard-replacement half of hyperhive#3310 only. The
swarm-ui half (rendering the CreateAgent DAG on the agent-creation page)
is downstream of hyperhive#3306/#3124 landing - no swarm-ui page exists
yet to mount it in.
2026-08-16 15:18:25 +02:00
damocles
bad5285f2e fix(hive-forge): populate owner/repo in dependency add/remove body 2026-08-16 14:57:17 +02:00
atlas
b8e19a31b2 docs(swarm-queue-client): qualify the status-bucket intra-doc link
An unqualified `[`open_or_create`]` in the module-level doc does not
resolve once the `kv` feature is on, which is the only configuration
where the module is compiled at all — so `docs-rustdoc` failed in CI
while a default-feature `cargo doc` passed locally. Measured both ways:
kv off documents clean, kv on errors `no item named open_or_create in
scope`.

Qualifying the path fixes it without widening any visibility, which is
the rule that check exists to protect.
2026-08-16 13:52:43 +02:00
atlas
5820c0e7e6 fix(hive-c0re): keep the cause in the swarm-status boot warnings
Same review catch as the crate side: these two format the queue client's
own error with `{:#}`, and thiserror's Display ignores the alternate flag,
so the source was silently dropped. The banners read "swarm status
publishing is off: swarm queue is half-configured" with no list of missing
variables, and "...: connecting to the swarm queue at <url>" with no nats
error saying why.

These are the two worst places to lose it. `set_boot_warning` is for a
one-shot startup step with no retry: the banner leaks until the process
restarts, so it is the operator's whole account of what went wrong.
2026-08-16 13:14:03 +02:00
atlas
3273971328 refactor(swarm-queue-client): typed errors for the bucket and the guard
Finishes the anyhow removal for the parts this branch adds: the status
bucket's open-or-create and the connected-client precondition. Two
variants, one of them behind the `kv` feature because the error type it
wraps does not exist without it — the error enum respects the same gate
the module does.

NotConnected is deliberately distinct from Connect: one is a connect that
was attempted and refused, the other is a request made before any
connection exists. The first is a deployment problem and the second is a
caller-ordering one, which is the whole reason a caller wants an enum
rather than a string.

The controller's `store` now returns the queue client's error rather than
an anyhow one: `OnceCell::get_or_try_init` takes its error type from the
closure, so widening there would mean converting inside the closure for
no gain. `view` `?`s it and anyhow converts at that boundary — the
library keeps a typed error, the binary keeps anyhow, and no call site
pays for the split.
2026-08-16 13:14:03 +02:00
atlas
c028b2ecfc docs(swarm): say how to make a hive report, not that nothing does
The section carried a "nothing publishes yet" note that is now false, and
said nothing about the one thing an operator has to do.

Written to the reader's question rather than the author's: what to set,
what defaults on an all-in-one host, what has to be carried by hand to a
hive that is not the swarm host, and where to look when a hive goes quiet.
The identity and the publish cadence are stated because they constrain the
staleness threshold an operator picks; the mechanism behind them is not.
2026-08-16 13:14:03 +02:00
atlas
48f69fcdea feat(swarm): wire a hive's queue coordinates for status publishing
Three options, all three derived from ONE predicate — this host runs both
the queue and the IdP — so a defaulted set is all or nothing. Deriving
them per-service looks equivalent and is not: `enableRequiredServices`
turns on matrix and authelia but not nats, so an ordinary all-local hive
would resolve two of three and trip the assertion below. Making the
partial state unrepresentable is what keeps that assertion honest.

Deliberately not the shape swarm-controller uses. That module emits its
queue coordinates only when authelia and NATS are local, which is right
for a service that *is* a swarm-host service — but a hive is the one thing
in a swarm that routinely is not on the swarm host, so the same rule would
make status publishing work on exactly the deployment that needs it least.

There is no `enable`: three coordinates that are all set is the enable. An
extra flag would allow configured-but-off, which is one more state to
explain and one more way to be silently quiet.

A half-set trio is an eval error rather than a silent no-op, because its
runtime failure mode is the expensive kind — the daemon comes up fine,
never connects, and the hive reads never_reported on a dashboard nobody is
watching yet. With the defaults all-or-nothing, the assertion only ever
judges what an operator typed by hand.

The secret arrives by LoadCredential, not a copy: hive-c0re is a host
unit, so systemd hands it the file directly and the secret never gains a
second on-disk copy. The client id is not chosen here either — it is
`hive-<hiveName>`, the identity swarm-authelia.nix already declares for
every entry in the roster.
2026-08-16 13:14:03 +02:00
atlas
dc394b459d feat(hive-c0re): offer this hive's readiness to the swarm
The controller reads per-hive status out of a JetStream KV bucket and
nothing was writing one, so every hive rendered `never_reported`. This is
the half that makes the read path mean anything.

A hive offers; the controller never reaches down to collect. The gateway
has gone down in a way where every recovery channel ran through the one
broken thing, so a status path that depended on the controller would go
dark exactly when it is needed to diagnose the controller's own network.

What it publishes is what the hive already says about itself —
`warnings::readiness()`, the same value `/health/ready` serves. Nothing
here stamps a time: freshness is derived by the reader from when the value
landed, so a hive cannot make itself look fresher than it is, and a hive
with a wrong clock skews only its own payload.

The key is this hive's `hiveName`, which `swarm.nix` already asserts is a
key of `swarm.hives` — so a hive that evaluates at all publishes under a
name the roster knows, rather than by convention.

Publish first, then wait: a hive that has just come up is the one whose
status someone is looking at, and sleeping first would make every restart
read stale for a full interval. The interval is one decision with the
controller's staleness threshold, not two — a ratio of 2 means one lost
publish still reads fresh and two consecutive misses read stale.

Failures go to the dashboard banner through SweepHealth, debounced, at
`warn` and deliberately not `crit`: `crit` is what makes this hive report
itself degraded, and a hive that cannot reach the queue is not unhealthy —
the swarm's view of it is. Publishing `degraded` because the publish
failed would be both false and self-erasing on the next tick.
2026-08-16 13:12:59 +02:00
atlas
e23a70e488 refactor(swarm-queue-client): share the connected-client precondition
An unconnected client does not fail a JetStream request, it hangs on it:
`retry_on_initial_connect` hands back a client before it is usable, and a
request made in that window waits (measured: still going at 15s against a
queue that refuses the credential). The controller guarded its read path
against that inline. Every consumer of the queue needs the same guard, so
it is not one daemon's to keep.

It matters more off a request path than on one. A hung request inside a
periodic task never reaches its `select!`, so the shutdown branch becomes
unreachable and the task cannot be stopped at all — where a request path
merely times a poll out.

The test is `!= Connected`, never `== Disconnected`: a client that has
never connected sits in `Pending`, so the `Disconnected` form passes it
straight through to the hang it was written to prevent — which is exactly
the boot-order case the guard exists for. Not feature-gated;
`connection_state()` is core async-nats.
2026-08-16 13:12:59 +02:00
atlas
9c1cfafeb5 refactor(hive-c0re): one producer for the readiness verdict
`get_health_ready` computed "degraded iff any warning is crit" inline and
wrapped it in a private `ReadyBody`. The swarm status publisher needs the
same verdict, and the warnings module's own doc already states why it must
not compute its own: two systems independently deciding what counts as
unhealthy is how they end up disagreeing.

The disagreement would also be silent. Each side would look internally
consistent, and the day a second degraded condition is added to one of
them, the dashboard and the swarm view would report different things about
the same host with nothing to flag it.

`warnings::readiness()` is now the single producer and `Readiness` the
single type. `ReadyBody` is deleted rather than made public: the endpoint
keeps the part that genuinely is its own, the mapping onto an HTTP status
code, and serves the shared document as its body.
2026-08-16 13:12:59 +02:00
atlas
22659234c4 refactor(swarm-queue-client): share the hive-status bucket's name and shape
The bucket has two ends in two crates: a hive writes its own key, the
controller reads every key. `swarm-controller` declared the name as a
private const with a doc comment arguing that "reader and writer must
name the same bucket" — an argument the writer, in another crate, could
not obey.

The name is the mild half. Both ends do get-or-create, because either may
come up first on a fresh swarm and neither can assume the other has run.
Two `Config`s that drift means whichever end created the bucket wins and
the other's `get_key_value` succeeds against a bucket it did not ask for:
no error, no log, just a retention policy nobody chose. Sharing the
constructor gives that race one outcome.

Behind a default-off `kv` feature, so the crate's other consumer — the
auth-callout responder, which speaks the connect and nothing else — still
pulls neither `jetstream` nor `kv`. That was the actual reason the
feature was excluded when this crate was extracted; the flag preserves
it. The surface is deliberately narrow: one bucket's name and creation
config, not a general KV facade.
2026-08-16 13:12:59 +02:00
atlas
6712cdb796 fix(swarm): say certFingerprint was removed, not that it does not exist
A deployed config still set `swarm.hives.<hive>.certFingerprint`, deleted
along with the dashboard feature it served. The module system's answer was
`The option ... does not exist`, which tells an operator nothing about why
it went, whether it moved, or what replaces it.

Re-declared invisible and internal, with a top-level assertion naming the
hives that still carry it and explaining that the swarm root CA replaces
per-hive leaf pinning.

`lib.mkRemovedOptionModule` cannot do this job, and it is worth writing
down why: neither of its halves survives the move into a submodule. Its
`apply = throw` fires only when the value is read, and nothing reads this
any more — that being the point of removing it. Its `config.assertions`
half would land on a submodule that declares no `assertions` option. It is
a top-level tool. This is the same shape swarm-peers-removed.nix already
uses for the analogous `peers.<hive>.caCert`.

An error rather than a warning, because re-declaring the option is what
stops the unhelpful message — and on its own that would turn a config that
used to fail into one that quietly evaluates with the setting ignored,
which is worse than the error it replaced.
2026-08-16 13:08:11 +02:00
atlas
79bc198165 fix(swarm-queue-client): export chain, and use it where anyhow used to
Review catch: `anyhow::Error`'s Display special-cases `f.alternate()` to
walk the source chain; thiserror's derive does not, so `{e:#}` and `{e}`
render identically for the new error type. Every call site that held an
`anyhow::Error`, formatted it with `{:#}`, and now holds this crate's
error kept compiling, kept looking right, and silently dropped the cause.

`chain()` was written for exactly this and then left private, applied only
to the auth callback I happened to be editing. Its own doc comment argues
that dropping the source chain is wrong, which made it the one thing in
the PR that should not have had a scope of one.

The controller's "swarm queue unreachable" warning is the site this fixes
here; the stacked PR fixes the two boot-warning banners, which matter more
still — one-shot, no retry, and they leak until restart.
2026-08-16 12:47:22 +02:00
atlas
d71222c206 refactor(swarm-queue-client): a library's errors are an enum, not anyhow
Operator ruling: libs should not use anyhow. The queue connect was moved
here verbatim from swarm-controller, which is a binary, so it arrived
still wearing a binary's error handling — the move changed what the code
is without changing how it reports.

Callers get variants they can match on, split by what an operator does
about them: a half-configured environment is a deployment bug, a refused
token is an identity-provider config problem, an unreachable queue is a
network one. The binaries that consume this keep anyhow and `?` converts,
so nothing downstream is more verbose for it. Same split hive-claude uses.

One thing anyhow was doing unpaid: the auth callback hands async-nats a
plain string, and a Display that stops at the top message drops the cause
— the half that says why the mint failed. `chain()` walks the source
chain, which is what `{:#}` was doing before.
2026-08-16 12:47:22 +02:00
atlas
62b9c76d69 chore(swarm-controller): drop reqwest, dead since the queue connect moved
Its only user was queue.rs, which is now swarm-queue-client. An unused
Cargo.toml dependency is not a build error, which is exactly why it
survives: the next reader takes it as still needed and copies it
forward.
2026-08-16 12:47:22 +02:00
atlas
a9603214c2 refactor(swarm-queue-client): extract the queue connect into a shared crate
A hive publishing its own status needs the same connect the controller
already has - mint an authelia token, present it at CONNECT for the
callout responder, let async-nats re-run the callback per attempt. Only
the use differs: the controller reads, a hive writes.

Copying it would put credential handling in two places, and a
token-refresh fix would then have to be found twice. That is the same
reasoning that already put hive-sock-client in its own crate rather than
in each daemon that speaks to a unix socket.

`from_env` takes a prefix rather than hardcoding SWARM_CONTROLLER_*: the
variables belong to the consuming unit, since a NixOS module sets them
alongside its other options. What is shared is the RULE - all four
together or none at all - not the spelling. The half-set case gains a
test, because it is the case the rule exists for and it previously had
none.

No jetstream/kv feature on the crate: it ends at a connected client, and
what a consumer does with it should be visible in that consumer's own
Cargo.toml.

Behaviour-preserving, and proven that way rather than by inspection: the
full behavioural gate (real nats-server, credential rotation, mutation)
is 20/0 unchanged, and the controller's own tests still pass.
2026-08-16 12:47:22 +02:00
atlas
bc594a36ef fix(hive-forge): give the SSO-source unit the same TLS trust as forgejo
Registering the OIDC login source makes an outbound HTTPS call - the CLI
fetches <issuer>/.well-known/openid-configuration to validate the
provider before writing the row. That URL is a swarm service name served
under the swarm CA, which the default system store has never heard of.

SSL_CERT_FILE was set on forgejo.service and not on
forgejo-sso-source.service, so the web service trusted the chain and the
registration one-shot did not. Same binary, same host, different unit.
The result was a 100% reproducible failure that no restart could fix:
nothing about restarting a unit gives it an environment variable it
never had. Every recorded run since the feature landed has failed with
x509: certificate signed by unknown authority.

The trust belongs to every process that makes the call, not to the
service that happens to be the obvious consumer.
2026-08-15 22:01:46 +02:00
iris
c67100588e docs+nix: fix stale certFingerprint/HYPERHIVE_PEERS references (hyperhive#3294)
Follow-up to the plumbing removal: docs/swarm/README.md gets the
biggest rewrite (drops the whole "Fingerprint format" section, fixes
the runtime-effects list, the WireGuard config example + "what the
mesh does" bullet), docs/gateway.md and hive-gateway/options.nix drop
4 "needs no certFingerprint" mentions, swarm-peers-removed.nix's
migration-warning text no longer tells an upgrading operator to carry
a field over that no longer exists, swarm.nix/swarm-wireguard.nix/
swarm-controller.nix/swarm-controller's main.rs get comment fixes
where they described the now-removed HYPERHIVE_PEERS shape. Also
caught one more stale "peer hives" mention in docs/web-ui/README.md's
SW4RM tab description that the first pass on this issue missed.
2026-08-15 19:56:11 +02:00
iris
c2f8ee225d remove certFingerprint + HYPERHIVE_PEERS plumbing (hyperhive#3294)
Mara wanted the underlying plumbing gone too, not just the dashboard
display. Traced every consumer before cutting:

- certFingerprint (services.hyperhive.swarm.hives.<name>.certFingerprint):
  removed the nix option entirely. Its only consumer was the dashboard
  code removed in the previous commits.
- HYPERHIVE_PEERS: removed entirely — the env var itself, the whole
  block that built it in hive-c0re/environment.nix, and its entry in
  meta.rs's FORWARDED_VARS (which forwarded it into every agent
  container). Turned out to have zero real consumers, not just one:
  the docs claimed hive-agent::identity::peers() read it for qualified
  agent labels, but no such function exists — identity.rs only
  qualifies THIS agent's own label with HYPERHIVE_HIVE_DOMAIN, nothing
  peer-list-related. Grepped the whole hive-agent crate to confirm
  before removing.

services.hyperhive.swarm.peerHives (the nix option HYPERHIVE_PEERS was
built from) is untouched — swarm-wireguard.nix reads it directly for
the wg-hive mesh, a real and unrelated consumer.

Verified: cargo build/clippy/test -p hive-c0re -p swarm-controller all
clean (needed nix develop -c per the usual -lsqlite3 gap), all touched
nix files pass nix-instantiate --parse, and a throwaway nixosSystem
eval confirms the wireguard mesh still configures a peer's
wireguardAddress into wg-hive correctly with certFingerprint gone.
2026-08-15 19:55:29 +02:00