swarm: carry the agent queue credential from the host into the harness

hive-c0re stats the two files `swarm-bao-queue-agent` lands and forwards
them into every agent container as systemd credentials, and the harness
resolves a `QueueConfig` out of them at boot. Nothing connects yet.

A credential and not a bind mount, and the mode is what forces it: the
secret is root:0600 and the harness runs as the unprivileged agent user,
so a bind would deliver a file that user cannot open. nspawn's
`--load-credential` is read by the container manager as root and
re-exposed under the consuming unit's own `User=`. hive-c0re never reads
the bytes either way, which is just as well — it runs as `hive-core`.

Absent files stay legal and become visible rather than silent: the
publisher lives on the authelia host and mints on its first boot, so
"nothing at that path" is the ordinary early state of a swarm. c0re
forwards nothing and logs why; the harness logs that it has no queue.

The client id comes out of the delivered file rather than being rebuilt
from `hiveName` in nix, which is the agreement `swarm-secret-client`
states. `QueueConfig::from_env` wants it as a value, so the harness reads
the file itself — assigning the variable instead would need
`std::env::set_var` in a process that has already spawned threads.

Refs #3805
This commit is contained in:
atlas 2026-09-12 22:41:36 +02:00 committed by atlas
commit 353cdd9264
6 changed files with 436 additions and 7 deletions

View file

@ -700,6 +700,12 @@ const CANONICAL_INPUTS: &[&str] = &["nixpkgs"];
/// trivial to stub from tests (which build their own slice instead of
/// touching process-wide env).
const FORWARDED_VARS: &[&str] = &[
// Where the swarm queue is and where its tokens are minted, as an agent
// container reaches them. Both are addresses the *host* computes — the
// queue's from the bridge IP an agent routes over, never a loopback
// address, which inside a container is the agent itself.
"HIVE_AGENT_NATS_URL",
"HIVE_AGENT_OIDC_TOKEN_ENDPOINT",
"HIVE_FORGE_URL",
"HIVE_FORGE_PUBLIC_URL",
"HIVE_MATRIX_URL",
@ -724,6 +730,16 @@ const FORWARDED_VARS: &[&str] = &[
/// answer. Half-wiring this map is not a missing nicety; it is a value that
/// evaluates fine and is silently wrong.
const FORWARDED_VAR_OPTIONS: &[(&str, &str)] = &[
// Read at build time to decide whether the harness unit declares the
// queue credential at all — see `nix/agent-modules/queue.nix`. An agent
// whose option says no queue and whose env says otherwise logs the
// partial-config error rather than half-connecting, which is the same
// failure this map's doc describes and the reason both halves are wired.
("HIVE_AGENT_NATS_URL", "hyperhive.queue.natsUrl"),
(
"HIVE_AGENT_OIDC_TOKEN_ENDPOINT",
"hyperhive.queue.tokenEndpoint",
),
("HIVE_FORGE_URL", "hyperhive.forge.url"),
("HIVE_MATRIX_URL", "hyperhive.matrix.url"),
("HYPERHIVE_HIVE_NAME", "hyperhive.hiveName"),
@ -2027,6 +2043,37 @@ mod tests {
);
}
/// The queue coordinates travel as a pair and are wired through both
/// halves of the forwarded-var machinery: the env var the harness reads at
/// runtime and the option the harness *unit* is built from. An agent that
/// got only the env var would declare no credential and then report a
/// partial config — visible, but a rebuild away from working.
#[test]
fn queue_coordinates_are_forwarded_as_both_env_and_option() {
for var in ["HIVE_AGENT_NATS_URL", "HIVE_AGENT_OIDC_TOKEN_ENDPOINT"] {
assert!(
FORWARDED_VARS.contains(&var),
"{var} must be forwarded into every agent's env"
);
}
let mut out = String::new();
push_forwarded_var_options(
&mut out,
&[
("HIVE_AGENT_NATS_URL", "nats://10.42.0.1:4222".to_string()),
(
"HIVE_AGENT_OIDC_TOKEN_ENDPOINT",
"https://auth.t.local/api/oidc/token".to_string(),
),
],
);
assert_eq!(
out,
" hyperhive.queue.natsUrl = \"nats://10.42.0.1:4222\";\n\
\x20 hyperhive.queue.tokenEndpoint = \"https://auth.t.local/api/oidc/token\";\n"
);
}
#[test]
fn require_service_urls_accepts_a_rendered_forge_url() {
require_service_urls(&[