hyperhive/swarm-queue-client
Repository files (latest commit first)
Filename Latest commit message Latest commit date
atlas 93c7454bf5 swarm: name the deploy event and grant the controller its publish
The subject and its payload live in `swarm-queue-client` for the reason
the knowledge event's already does: three crates have to agree on the
string, and the one that agrees hardest — the auth-callout responder,
which decides whether the publish is permitted at all — speaks neither
`jetstream` nor `kv`.

Swarm-wide rather than a `$SWARM.deploy.<hive>` family. That family
would look like isolation and provide none: this responder scopes
publish only, leaving `sub` unrestricted, so a hive could subscribe to
another's subject as easily as to its own. Until `sub` is scoped the
split costs a wider grant and buys nothing, so the addressing goes in
the payload and each hive filters on its own name.

Unlike the knowledge event the message is addressed, so it carries a
payload — a trigger, never the config. The hive already tracks the
agent's config repo; desired state on the wire would make this a second
source of truth for something git owns, and a hive that missed a
message would be wrong rather than late.

Both test arms mirrored from the knowledge event. The negative one
matters more here: a forged knowledge event makes a hive re-read a
repo, a forged deploy event makes it rebuild and restart a named agent.
2026-08-31 00:17:41 +02:00
..
src swarm: name the deploy event and grant the controller its publish 2026-08-31 00:17:41 +02:00
Cargo.toml move otel_http_client from swarm-queue-client into swarm-controller 2026-08-29 11:17:24 +02:00
README.md refactor(swarm-queue-client): share the hive-status bucket's name and shape 2026-08-16 13:12:59 +02:00

swarm-queue-client

Connecting to the swarm message queue as an authenticated client. Shared by every process that participates: the swarm controller reads hive status out of the queue, a hive publishes its own status into it.

Why a crate and not a module per binary

The connect is identical for every participant — mint an authelia token, present it at CONNECT for the auth_callout responder to introspect, let async-nats re-run the callback on each connection attempt. Only the use differs.

Two copies of that would be two copies of credential handling, and a token-refresh fix would have to be found twice. The same reasoning already put hive-sock-client in its own crate rather than in each daemon that speaks to a unix socket.

The two properties that constrain the code

A token expires. Authelia issues client_credentials access tokens with expires_in: 3599. Authentication happens at CONNECT, so a long-lived connection is fine — but a reconnect an hour later needs a token minted an hour later.

The refresh therefore lives in the auth callback, not in a timer. async-nats invokes it per connection attempt, so there is no window in which the client holds a token it minted for a previous connection. The alternative — mint once, own the reconnect loop — fails in the way this subsystem exists to prevent: the process keeps serving while its data quietly stops moving, and nothing says so until someone reads a dashboard.

Configuration

QueueConfig::from_env(prefix) reads <prefix>_NATS_URL, <prefix>_OIDC_TOKEN_ENDPOINT, <prefix>_OIDC_CLIENT_ID and <prefix>_OIDC_CLIENT_SECRET_FILE.

The prefix is a parameter because the variables belong to the consuming unit — a NixOS module sets them alongside its other options. What is shared is the rule, not the spelling: all four together or none at all. A half-set environment is a hard error, because the failure it would otherwise produce is the expensive kind — the process comes up "fine", never connects, and the data it was supposed to move silently stops.

The client secret is a path, not a value: putting it in the environment would publish it to anything that can read /proc/<pid>/environ. It is read per token request rather than cached, so a rotation the operator believes took effect actually did.

What this crate does not do

It ends at a connected client. jetstream/kv are off by default — what a consumer does with the connection is its own business, and its Cargo.toml is where that requirement should be visible. The auth-callout responder speaks the connect and nothing else, and pays for nothing else.

The one exception: the kv feature

kv adds status, which holds the name and the creation config of the hive-status bucket — nothing more.

It is here because that bucket has two ends in two crates: a hive writes its own key, the controller reads every key. The name being a repeated literal is the mild half of the problem; the sharp half is that either end may arrive first on a fresh swarm, so both create the bucket if it is missing. Two Configs that drift means whichever end created it wins and the other opens a bucket it did not ask for — no error, no log, just a retention policy nobody chose.

An agreement between two crates has to live in one of them, and neither end of this bucket is senior to the other. Behind a default-off feature, the consumer that needs none of it still pays nothing.