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
Delete nix/templates/ruth.nix, the ruth nixosConfiguration, module and
toplevel, the hive-c0re managerToplevel option, and the meta flake's
isManager switch with lifecycle::MANAGER_NAME. Every agent, ruth
included, now builds from nix/templates/agent.nix. The template's only
content beyond agent.nix was docs.enable = mkDefault true; docs.enable
is off by default for every agent and ruth's own agent.nix turns it on.
Refs #5377
hive-c0re's admin socket and hive-agent's in-agent socket (mode 0666) read
the request with an unbounded read_line: no length cap and no deadline, so
a peer that never sends a newline grows a buffer and pins a task forever.
The bounded reader (16 MiB cap) moves from hive-c0re's socket_server into
hive-sock-client, the one crate both daemons already share or can take, and
gains optional deadlines for the first byte and for the rest of the line.
Both sockets now use it and answer an over-long or stalled line with the
same error reply the bounded sockets give, then close. The admin socket
bounds only the started line (clients may idle between requests); the
in-agent socket is single-shot and bounds silence too. The per-agent
sockets keep their behaviour (no deadline).
After a cut, Tail::finish dropped everything through the first newline.
When the stream ended in one line longer than the window, that newline
was the last byte, so the window came back empty and the reply fell back
to the bare exit status. The cut line is now dropped only when something
follows it.
nix_build::run read nix's stdout and stderr with read_to_end, so a long
--print-build-logs run held its whole log in memory until nix exited,
though the reply uses only the last 30 lines (4000 bytes) of stderr and
the last non-empty line of stdout. Both streams now go through a Tail
that holds at most 128 KiB and keeps the last 64 KiB. Output under 64 KiB
reaches tail() whole, so those replies are unchanged.
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
forgejo-api 0.11.1's sync client builds its own reqwest client with no
timeout and takes no caller-supplied client (sync.rs: new and
with_user_agent are the only constructors), so a forge that accepts a
connection and then stalls blocked every typed verb forever.
A watchdog thread now starts before the client is built. After
HIVE_FORGE_DEADLINE seconds (default 900, five minutes above
DOWNLOAD_TIMEOUT) it prints a hive-forge: FAILED: line and exits 75.
It bounds the whole command, not each request, and a write may already
have landed when it fires; the help text and docs/tools/forge.md say so.
Not every swarm's operator is she/her. The prompt now names no pronouns
and tells agents to say "the operator"; pronouns will come from a
per-operator profile setting. The operatorPronouns removal warning and
claude-invocation.md say the same.
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.
Both are ignored with a warning through the same shim, now
nix/host-modules/hive-options-removed.nix.
- otel.metricIntervalMs: claude in every agent and hive-c0re's
container-resource exporter run at the 60s default. The agent-level
services.hyperhive.agent.otel.metricIntervalMs, set only by the meta
render, goes too (warning shim in nix/agent-modules/otel-removed.nix).
- otel.debug: services.hyperhive.agent.otel.debug stays, documented and
set in an agent's own config; the meta render no longer emits it.
Both options are now ignored with a warning (the swarm-peers-removed.nix
shape), so a host that still sets one keeps evaluating.
- operatorPronouns: the agents' system prompt names the pronouns as
she/her, the value the harness fell back to and the option's default.
The --operator-pronouns flag, HiveEnv field, meta-flake let binding
and HIVE_OPERATOR_PRONOUNS env var are gone.
- contextWindowTokens: the per-family defaults (haiku 200000, sonnet
1000000, opus 1000000) move into hive-agent's harness_state as a
table; the window the API reports after a turn still wins. The
--context-window-tokens flag, HiveEnv field and the rendered
HIVE_CONTEXT_WINDOW_TOKENS_<KEY> env vars are gone.
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.
the manager socket no longer exists; ruth's socket is the per-agent one. Fixes the wire-type crate docs, hive-sh4re manager.rs header and README, and two other mentions.
start_manager bound the same path register_agent binds. The migrate branch, hive-priv-sock::MANAGER_NAME and MANAGER_CONTAINER were no-ops or dead. The send allow-list docs promised a manager exemption the code never had. Tests that used ruth as the manager fixture use plain names.
ruth is created from the swarm dashboard like any agent. An existing ruth container starts at boot through the boot reconcile, like every other agent. A definition of services.hyperhive.ruthless now fails evaluation with a removal message.
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
The dashboard filtered by hive and agent only, so the review classifier's
API-key spend could not be told apart from the agents' subscription usage.
Add a service variable (agents or the review classifier) and apply it to
every panel and to the agent variable's own query.
Drop the H0M3 tile hub and the core page. The dashboard is now the index;
kept state is its fifth tab. A pages menu in the chrome links flow, stats,
API docs and the swarm UI. The swarm apex URL comes from
services.hyperhive.swarm.domain via HIVE_SWARM_PUBLIC_URL and the new
swarm_public_url field of the state snapshot.
A pki start refused by its start limit failed every reader that
Requires= it with `dependency`. That leaves a unit inactive, not
failed, so Restart=on-failure never fired and readers without a
refresh timer never ran again (#5381: grafana 502 for ~43 min).
Readers and granting units now Wants= + After= pki; one whose leaf is
still missing fails its login with exit 1 into its existing retry.
pki gets StartLimitIntervalSec=0: it is deliberately not
RemainAfterExit, so each reader's start re-runs it.
`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.
copy_stream_to_file dropped the copy and flush errors, so an ENOSPC or
pipe error left a truncated .out/.err with no trace while the task still
reported its exit status. It now returns the error and the runner logs
it with the stream and file path.
An answer that fails to validate is asked for a second time, for that
classification only, with a reminder to reply with the JSON object alone.
The second ask comes out of the classification's CLASSIFICATION_DEADLINE:
it gets what the first ask left, and is skipped when less than 20 seconds
remain, so RUN_DEADLINE's bound still holds. A failed ask (runtime error,
timeout) is not repeated. If the second answer does not validate either,
the row has no answer, as before.
Model::ask takes the deadline for the ask; the ACP runtime no longer holds
its own.
The parser also takes the first fenced block of a reply that has prose
around the fence; prose around a bare object is still rejected.
Adds LockPersonality, RestrictRealtime, ProtectKernelLogs and
ProtectKernelModules to the hive-priv unit. Nothing in hive-priv's
sources or its children uses personality(2), real-time scheduling,
/dev/kmsg, /proc/kmsg or explicit module loading.
A module-eval case pins the four directives on the rendered unit.
start_many mapped every read error on the marker to None via .ok(), so the
agent went down the rebuild path while the log blamed a stale rev. A missing
marker stays silent; any other error now logs the path and error. Control
flow is unchanged.
The metrics store's container now runs the journal forwarder the other
service containers run, pushing to a receiver of its own on the swarm
collector (serviceProducers swarm-victoriametrics) as a new machine OIDC
client (swarm.victoriametrics.otel.clientId), registered by a glue file
wherever authelia runs and published to the store by the secret publisher.
The container keeps its loopback-only netns. The collector cannot dial a
unix socket, so swarm-victoriametrics-egress-relay, a socket-activated
systemd-socket-proxyd inside the container, takes its connections on
127.0.0.1:4380 and passes them to /run/swarm-victoriametrics-egress/egress.sock.
The host's nginx listens there and sends /swarm-victoriametrics/ to the swarm
collector and /api/oidc/token to authelia, both by name and verified, and
refuses every other path. nginx gets ReadWritePaths on that directory and
removes a stale socket before it starts.
The container cannot reach the store, so the client secret is fetched on the
host by swarm-victoriametrics-forwarder-oidc under swarm-metrics-auth's leaf,
the shape swarm-bao-forwarder-oidc has, and bound into the container
read-only. The forwarder module gains hostSecretFile for that: set, it renders
no fetch unit. swarm-metrics-auth's policy gains one read stanza on the
forwarder's client path.
The container drops --link-journal=host, and the host collector stops reading
it.
Also lists the forge publisher's and matrix-ctl's leaves in the forwarder
secrets row of docs/swarm/secrets.md, which the hive-forge/hive-matrix change
missed.
The reader loops used while-let Ok(Some(line)), so a read error ended them
exactly like EOF with nothing logged. next_line now warns on Err, naming
the stream, and returns None; the close path is unchanged.
Only a missing file counts as 'marker absent'; any other read error (EACCES,
EIO, non-UTF-8) used to read as empty, so the agent's flake.nix was replaced
and committed.
find_space_by_name and find_chat_room_by_name carried the same 52-line
body and differed only in the space test and the name. Both now call
find_joined_room(base, want_space, name). Same requests, same order,
same return values.
The catch-all, the dashboard and the hive-domain vhost were built in
vhosts.nix with the listen set, TLS attrs and headers applied by hand.
They are declared under services.hyperhive.gateway.vhosts now, so every
vhost behind the gateway goes through declared-vhosts.nix.
Two gateway-side rules cover what the catch-all needed: a vhost can set
securityHeaders = false (the catch-all only returns 444), and `default`
is rendered at mkDefault priority so an operator's own default vhost
still wins. The matrix discovery locations are unchanged.
The rendered nginx vhosts are unchanged.
The one-shot marker was written with create_dir_all(..).ok() and
fs::write(..).ok(), so a failure was invisible and the avatar upload
repeated on every start. Log the path and error; startup still succeeds.
Each container now runs the journal forwarder the CI runner's, Grafana's and
the log store's containers run, pushing to a receiver of its own on the swarm
collector (serviceProducers hive-forge and hive-matrix). Each authenticates as
a new machine OIDC client of its own (swarm.forge.otel.clientId,
swarm.matrix.otel.clientId), registered by a glue file wherever authelia runs,
minted by authelia and published to the store by the secret publisher. The
forge's and the homeserver's browser login clients are unchanged. The client
ids are swarm-forge-collector and swarm-matrix-collector, not
hive-<x>-collector, which is the client id a hive named <x>-collector gets.
The fetch unit inside each container reads the client secret with the store
identity the host already holds for that service. The forge publisher's leaf
is handed into the forge container as an nspawn system credential, as the
grafana and log store containers do. matrix-ctl's leaf is already bound into
the homeserver's container, so its forwarder reads it at those paths, as
swarm-matrix-appservice-publish does; with no matrix-ctl identity the
homeserver's forwarder is off. Each of the two policies gains one read stanza
on its forwarder's client path. The host units that use those identities are
unchanged.
The two containers drop --link-journal=host, and the host collector stops
reading them.
Ten service modules (forge, matrix chat, authelia, grafana, otel, nats,
swarm-ui, victorialogs, victoriametrics, bao UI) wrote
services.nginx.virtualHosts themselves, each applying the gateway's
listen set, per-name TLS attrs and security headers by hand. They now
declare services.hyperhive.gateway.vhosts.<name> with only their own
locations and options; ./declared-vhosts.nix renders it into the nginx
vhost with listen, TLS and headers added.
An eval assertion refuses a declared vhost that sets listen, addSSL,
onlySSL, enableACME, useACMEHost, sslCertificate or sslCertificateKey,
and one declared while the gateway is off. nats' own leaf is the new
`certificate` field. The rendered nginx vhosts are unchanged.
open_stream takes its deadlines as parameters so the tests can use short
ones. The header-write test connects to a peer that holds the connection
without reading and sends a header larger than any loopback buffer; the
connect deadline is tested through the shared bounded helper with a
future that never completes, since a connect that hangs is not
reproducible without the network. The module-eval case asserts the
unit's RuntimeMaxSec and that the receiver script reads its header with
a timeout.
The receiver blocked in read until a peer sent a header, and a stalled
btrfs receive was never reaped, so a silent or stuck connection held a
root service instance indefinitely. The receiver now gives the header 30 s
and the unit has RuntimeMaxSec=6h.
The sender had no deadline on the TCP connect or the header write, so an
unreachable mesh address held the admin-socket handler until the kernel
gave up. Both now time out after 15 s and report it like any other
failure. The stream body stays unbounded on the sender: the receiver's
RuntimeMaxSec already caps it, and btrfs send writes it outside this
process.