Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/swarm-queue-client
Repository files (latest commit first)
Filename Latest commit message Latest commit date
atlas 2c5eef73cd review classifier: model, effort and review concurrency as swarm-wide settings
The classifier ran every classification on a hard-coded `haiku` with no
`--effort`, and the controller ran at most two requested reviews at once,
also hard-coded, so changing any of them took a code change and a
redeploy.

swarm-controller now keeps one settings document,
`{model, effort, max_concurrent_reviews}`, in its own
`review-classifier-settings` KV bucket under `all`, the way it keeps the
schedule definitions. No document means today's values: haiku, no
`--effort`, two reviews. `GET /api/review-classifier/settings` reads it;
`PUT` replaces it whole, needs the gateway's `Remote-User` like the
try-out POST, and refuses a bad model (the inference route's check), an
effort outside the five levels or a limit outside 1..=8 with
problem+json 400.

Every run request (try-out and requested review) now carries the model
and effort, and the classifier passes them to claude instead of its
constant, which is gone. A try-out reads the settings when it is posted;
a review reads them for the limit before it waits for a place and again
for the model and effort once it has one. The review semaphore is
replaced by a running count under a resizable limit, so a PUT, or the
next review's read, changes how many run without a restart, and a lower
limit lets the runs already going finish. MAX_WAITING_REVIEWS is
unchanged.

Closes #5151, part of #5096.
2026-10-13 01:45:19 +02:00
..
src review classifier: model, effort and review concurrency as swarm-wide settings 2026-10-13 01:45:19 +02:00
Cargo.toml swarm-controller: GET/PUT/DELETE /api/agents/{name}/limits 2026-10-12 20:22:21 +02:00
README.md hive status publishing: read the hive's OIDC secret from the store, retry until it is there 2026-10-10 01:05:33 +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

A caller builds a QueueConfig itself from its own environment: the queue's URL, the token endpoint and the client id, plus an optional CA file. The variables belong to the consuming unit, so each daemon reads them under its own prefix. What is shared is the rule, not the spelling: the three coordinates 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 never in the environment, which would publish it to anything that can read /proc/<pid>/environ. A caller passes it either as a path (ClientSecret::File), read per token request rather than cached, so a rotation the operator believes took effect actually did, or as a value it fetched itself (ClientSecret::Value), held in memory. swarm-controller and hive-c0re both do the latter with the secret they read from the swarm's secret store.

Token request shape: HTTP Basic, audience, scope

token_request builds a client_credentials grant, authenticated with HTTP Basic — never the form body. Both are legal OAuth 2.0 (client_secret_basic vs client_secret_post), but a client registration names one, and authelia's default (and ours) is Basic. Sending the credentials in the body once got every token request refused with Client authentication failed … the registered client is configured to only support 'client_secret_basic', which reached the operator as an endless 429 because the retries tripped a rate limiter whose penalty grew faster than the retry interval — the 429 arrived before the credentials were ever evaluated, so the line naming the real cause appeared once an hour. RFC 6749 §2.3.1 says clients SHOULD use Basic, both introspection callers in this workspace already do, and a secret in a header is one fewer place for a proxy to log it.

audience (RFC 8707 resource indicators) and scope are both opt-in; None for either reproduces the request this crate sent before the parameter existed. Omitted, audience gets whatever authelia defaults a scopeless client_credentials grant to — the queue connection's own case, which has always worked without asking. A caller proving this identity to a specifically audience-checked receiver (the swarm-otel oidc/swarm authenticator being the first one) has to ask by name, the same way swarm-otel.nix's own prometheus scrape config already does per target (endpoint_params.audience): a token minted without asking carries aud: [], and an audience-checked receiver refuses that just as readily as the wrong one. scope follows the same rule and travels with audience: registration is not issuance, and the authorisation server grants no scope the client never requested. A caller reaching a destination behind authelia's /api/authz/auth-request needs authelia.bearer.authz here however completely the client is registered for it — the failure swarm-otel.nix records against its own client (a scopeless token refused at introspection with "the requested scope is invalid, unknown, or malformed") is the same one, one layer down.

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.