From c894192e4f8eb1e469ea8a4a35e99ea58cccddaf Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 16 Sep 2026 14:42:38 +0200 Subject: [PATCH 1/4] swarm-controller: add configured default matrix homeserver URL Adds services.hyperhive.deploy.swarm-controller.matrixHomeserverUrl, threaded to the daemon as SWARM_CONTROLLER_MATRIX_HOMESERVER_URL, and a Rust helper (homeserver_or_configured_default) that lets a caller-supplied homeserver keep overriding it. Config plumbing only: put_matrix_account does not call the helper yet, so this is a no-op for every current caller. Refs #4345 --- nix/host-modules/swarm-controller.nix | 28 +++++++++++++ swarm-controller/src/main.rs | 11 +++++ swarm-controller/src/matrix_account.rs | 58 +++++++++++++++++++++++++- 3 files changed, 95 insertions(+), 2 deletions(-) diff --git a/nix/host-modules/swarm-controller.nix b/nix/host-modules/swarm-controller.nix index 05f7f7dd..42ac308d 100644 --- a/nix/host-modules/swarm-controller.nix +++ b/nix/host-modules/swarm-controller.nix @@ -192,6 +192,15 @@ let SWARM_CONTROLLER_AUTH_BRIDGE_URL = deployCfg.swarm-controller.authBridgeUrl; }; + # Swarm-wide default for `PUT .../matrix-accounts/{account}`'s own + # `homeserver` field, for a request that omits one. Same shape as + # `authBridgeEnv` above: genuinely optional, gated on the option + # resolving rather than assumed. Not read by anything yet — see the + # option's own description. + matrixHomeserverEnv = lib.optionalAttrs (deployCfg.swarm-controller.matrixHomeserverUrl != null) { + SWARM_CONTROLLER_MATRIX_HOMESERVER_URL = deployCfg.swarm-controller.matrixHomeserverUrl; + }; + # The swarm's own display name, for `GET /api/swarm` (swarm-ui's chrome). # Gated on the option resolving, not defaulted to an empty string: an # operator who never named the swarm gets `name: null` from the @@ -520,6 +529,24 @@ in ''; }; + matrixHomeserverUrl = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "https://matrix.example.org"; + description = '' + Swarm-wide default homeserver for `PUT + .../matrix-accounts/{account}` requests that omit their own + `homeserver` — see that route's own doc comment + (`swarm-controller/src/matrix_account.rs`) for why the field is + optional in token mode and what omitting it currently resolves to. + + `null` (the default) leaves that per-request resolution exactly as + it is today. **Not yet consulted by the route at all**: this option + only exists to carry the value in, ahead of the route being taught + to fall back to it. + ''; + }; + baoClientCertFile = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; @@ -859,6 +886,7 @@ in // webhookEnv // authBridgeEnv // swarmNameEnv + // matrixHomeserverEnv // baoEnv // otelEnv; }; diff --git a/swarm-controller/src/main.rs b/swarm-controller/src/main.rs index 0f254506..eed7419c 100644 --- a/swarm-controller/src/main.rs +++ b/swarm-controller/src/main.rs @@ -1621,6 +1621,17 @@ async fn main() -> Result<()> { } }; + // Diagnostic only — `put_matrix_account` does not call this yet (see its + // own doc comment), so this cannot change any request's outcome. Logged + // so an operator can tell the config landed before the slice that + // consults it does. + if matrix_account::homeserver_or_configured_default(None).is_none() { + tracing::debug!( + "{}; matrix-account requests still need their own homeserver", + matrix_account::DEFAULT_HOMESERVER_ENV + ); + } + let config_prs = forge_client.clone().map(config_pr::spawn); let state_forge = keep_forge_for_state(forge_client, webhook_secret.clone()); diff --git a/swarm-controller/src/matrix_account.rs b/swarm-controller/src/matrix_account.rs index 46f8d38c..986998fe 100644 --- a/swarm-controller/src/matrix_account.rs +++ b/swarm-controller/src/matrix_account.rs @@ -35,6 +35,23 @@ fn default_mode() -> String { "token".to_owned() } +/// Env var the controller's NixOS module sets from +/// `services.hyperhive.deploy.swarm-controller.matrixHomeserverUrl` — the +/// swarm-wide default `PutMatrixAccountRequest::homeserver` falls back to +/// when a caller omits one. +/// +/// Not yet consulted by [`put_matrix_account`]: `homeserver_or_configured_default` +/// below exists for a later slice of hyperhive#4345 to call; this one only +/// wires the config through. +pub(crate) const DEFAULT_HOMESERVER_ENV: &str = "SWARM_CONTROLLER_MATRIX_HOMESERVER_URL"; + +/// `caller`'s own homeserver, or [`DEFAULT_HOMESERVER_ENV`] when the caller +/// left it unset. `caller` always wins — this only fills a gap it left, +/// never replaces a value it gave. +pub(crate) fn homeserver_or_configured_default(caller: Option) -> Option { + caller.or_else(|| std::env::var(DEFAULT_HOMESERVER_ENV).ok()) +} + /// The credential to store for one agent's external matrix account. /// /// No `Debug` derive — matching `hive-c0re::dashboard::matrix_accounts`'s @@ -339,8 +356,8 @@ async fn matrix_password_login( #[cfg(test)] mod tests { use super::{ - PutMatrixAccountRequest, is_reserved_account, password_fields, resolve_credential, - token_credential, + DEFAULT_HOMESERVER_ENV, PutMatrixAccountRequest, homeserver_or_configured_default, + is_reserved_account, password_fields, resolve_credential, token_credential, }; fn request(mode: &str) -> PutMatrixAccountRequest { @@ -353,6 +370,43 @@ mod tests { } } + /// SAFETY: single-threaded mutation of a process env var no other test + /// in this crate reads; restored (removed) before returning. + #[test] + fn caller_supplied_homeserver_wins_even_with_a_default_configured() { + unsafe { + std::env::set_var(DEFAULT_HOMESERVER_ENV, "https://default.example.org"); + } + let result = + homeserver_or_configured_default(Some("https://caller.example.org".to_owned())); + unsafe { + std::env::remove_var(DEFAULT_HOMESERVER_ENV); + } + assert_eq!(result.as_deref(), Some("https://caller.example.org")); + } + + /// SAFETY: same as above. + #[test] + fn falls_back_to_the_configured_default_when_the_caller_omits_one() { + unsafe { + std::env::set_var(DEFAULT_HOMESERVER_ENV, "https://default.example.org"); + } + let result = homeserver_or_configured_default(None); + unsafe { + std::env::remove_var(DEFAULT_HOMESERVER_ENV); + } + assert_eq!(result.as_deref(), Some("https://default.example.org")); + } + + /// SAFETY: same as above. + #[test] + fn none_when_neither_caller_nor_default_is_set() { + unsafe { + std::env::remove_var(DEFAULT_HOMESERVER_ENV); + } + assert_eq!(homeserver_or_configured_default(None), None); + } + #[test] fn main_is_reserved_but_nothing_else_is() { assert!(is_reserved_account("main")); From 24ca14fd4a9a5c0b46adee0688393003969fba96 Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 16 Sep 2026 14:45:07 +0200 Subject: [PATCH 2/4] ci.yml: note that CI deliberately does not run on main Refs #4345 --- .forgejo/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index fc4ec447..8cb2b2f0 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -1,6 +1,7 @@ name: CI on: + # No `push` trigger, so nothing re-validates a merge commit on main — an accepted infra-load tradeoff (mara, #3938, 2026-09-16), not an oversight. pull_request: branches: ["**"] # Lets `hive-forge ci-rerun` re-trigger CI via workflow-dispatch API From 87af0f38d3bd4cbf7f6f815e9ab39aa0362c7ec8 Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 16 Sep 2026 14:51:30 +0200 Subject: [PATCH 3/4] swarm-controller: make the homeserver default fn pure, fixing test race homeserver_or_configured_default read DEFAULT_HOMESERVER_ENV internally, so its three unit tests raced each other by set_var/remove_var-ing the same process env var with no synchronization under cargo test's default parallelism (argus, PR #4443 review). Take the default as a plain parameter instead of reading the env var inside the function. The one env read moves to a new configured_default_homeserver() helper, called once at the edge (main.rs's startup diagnostic); homeserver_or_configured_default itself is now pure and its tests need no env mutation at all. Refs #4345 --- swarm-controller/src/main.rs | 7 ++- swarm-controller/src/matrix_account.rs | 69 ++++++++++++++------------ 2 files changed, 44 insertions(+), 32 deletions(-) diff --git a/swarm-controller/src/main.rs b/swarm-controller/src/main.rs index eed7419c..99f43687 100644 --- a/swarm-controller/src/main.rs +++ b/swarm-controller/src/main.rs @@ -1625,7 +1625,12 @@ async fn main() -> Result<()> { // own doc comment), so this cannot change any request's outcome. Logged // so an operator can tell the config landed before the slice that // consults it does. - if matrix_account::homeserver_or_configured_default(None).is_none() { + if matrix_account::homeserver_or_configured_default( + None, + matrix_account::configured_default_homeserver(), + ) + .is_none() + { tracing::debug!( "{}; matrix-account requests still need their own homeserver", matrix_account::DEFAULT_HOMESERVER_ENV diff --git a/swarm-controller/src/matrix_account.rs b/swarm-controller/src/matrix_account.rs index 986998fe..263672d3 100644 --- a/swarm-controller/src/matrix_account.rs +++ b/swarm-controller/src/matrix_account.rs @@ -38,18 +38,36 @@ fn default_mode() -> String { /// Env var the controller's NixOS module sets from /// `services.hyperhive.deploy.swarm-controller.matrixHomeserverUrl` — the /// swarm-wide default `PutMatrixAccountRequest::homeserver` falls back to -/// when a caller omits one. +/// when a caller omits one. Read via [`configured_default_homeserver`], not +/// directly — see that fn's doc. /// /// Not yet consulted by [`put_matrix_account`]: `homeserver_or_configured_default` /// below exists for a later slice of hyperhive#4345 to call; this one only /// wires the config through. pub(crate) const DEFAULT_HOMESERVER_ENV: &str = "SWARM_CONTROLLER_MATRIX_HOMESERVER_URL"; -/// `caller`'s own homeserver, or [`DEFAULT_HOMESERVER_ENV`] when the caller -/// left it unset. `caller` always wins — this only fills a gap it left, -/// never replaces a value it gave. -pub(crate) fn homeserver_or_configured_default(caller: Option) -> Option { - caller.or_else(|| std::env::var(DEFAULT_HOMESERVER_ENV).ok()) +/// `caller`'s own homeserver, or `default` when the caller left it unset. +/// `caller` always wins — this only fills a gap it left, never replaces a +/// value it gave. +/// +/// Takes `default` as a plain parameter rather than reading +/// [`DEFAULT_HOMESERVER_ENV`] itself: a caller wants that env read done once, +/// at the edge (see [`configured_default_homeserver`]), and keeping this fn +/// pure makes it testable without mutating shared process env — three tests +/// doing exactly that raced each other under `cargo test`'s default +/// parallelism in review. +pub(crate) fn homeserver_or_configured_default( + caller: Option, + default: Option, +) -> Option { + caller.or(default) +} + +/// Reads [`DEFAULT_HOMESERVER_ENV`] fresh each call — the one place this +/// crate touches that env var, so [`homeserver_or_configured_default`] above +/// can stay a pure function. +pub(crate) fn configured_default_homeserver() -> Option { + std::env::var(DEFAULT_HOMESERVER_ENV).ok() } /// The credential to store for one agent's external matrix account. @@ -356,8 +374,8 @@ async fn matrix_password_login( #[cfg(test)] mod tests { use super::{ - DEFAULT_HOMESERVER_ENV, PutMatrixAccountRequest, homeserver_or_configured_default, - is_reserved_account, password_fields, resolve_credential, token_credential, + PutMatrixAccountRequest, homeserver_or_configured_default, is_reserved_account, + password_fields, resolve_credential, token_credential, }; fn request(mode: &str) -> PutMatrixAccountRequest { @@ -370,41 +388,30 @@ mod tests { } } - /// SAFETY: single-threaded mutation of a process env var no other test - /// in this crate reads; restored (removed) before returning. + // `homeserver_or_configured_default` takes its default as a plain + // parameter rather than reading the env var itself, so these are pure + // — no process env mutation, and so no risk of racing each other (or + // any other test in the crate) under `cargo test`'s default parallelism. + #[test] fn caller_supplied_homeserver_wins_even_with_a_default_configured() { - unsafe { - std::env::set_var(DEFAULT_HOMESERVER_ENV, "https://default.example.org"); - } - let result = - homeserver_or_configured_default(Some("https://caller.example.org".to_owned())); - unsafe { - std::env::remove_var(DEFAULT_HOMESERVER_ENV); - } + let result = homeserver_or_configured_default( + Some("https://caller.example.org".to_owned()), + Some("https://default.example.org".to_owned()), + ); assert_eq!(result.as_deref(), Some("https://caller.example.org")); } - /// SAFETY: same as above. #[test] fn falls_back_to_the_configured_default_when_the_caller_omits_one() { - unsafe { - std::env::set_var(DEFAULT_HOMESERVER_ENV, "https://default.example.org"); - } - let result = homeserver_or_configured_default(None); - unsafe { - std::env::remove_var(DEFAULT_HOMESERVER_ENV); - } + let result = + homeserver_or_configured_default(None, Some("https://default.example.org".to_owned())); assert_eq!(result.as_deref(), Some("https://default.example.org")); } - /// SAFETY: same as above. #[test] fn none_when_neither_caller_nor_default_is_set() { - unsafe { - std::env::remove_var(DEFAULT_HOMESERVER_ENV); - } - assert_eq!(homeserver_or_configured_default(None), None); + assert_eq!(homeserver_or_configured_default(None, None), None); } #[test] From 72bff26336f0ef18c0e25c0950b698f00c0a3f60 Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 16 Sep 2026 14:53:44 +0200 Subject: [PATCH 4/4] fix tracker refs flagged by check-issue-refs.sh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reworded the two hits scripts/check-issue-refs.sh found — a bare hash- number tag in .forgejo/workflows/ci.yml's push-trigger comment and a hyperhive#4345 tag in matrix_account.rs's doc comment — into prose that stands on its own, per the lint's own rule. Neither carried semantic weight beyond what the prose already says once reworded. Refs #4345 --- .forgejo/workflows/ci.yml | 2 +- swarm-controller/src/matrix_account.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 8cb2b2f0..0ed83313 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -1,7 +1,7 @@ name: CI on: - # No `push` trigger, so nothing re-validates a merge commit on main — an accepted infra-load tradeoff (mara, #3938, 2026-09-16), not an oversight. + # No `push` trigger, so nothing re-validates a merge commit on main — an accepted infra-load tradeoff mara signed off on, not an oversight. pull_request: branches: ["**"] # Lets `hive-forge ci-rerun` re-trigger CI via workflow-dispatch API diff --git a/swarm-controller/src/matrix_account.rs b/swarm-controller/src/matrix_account.rs index 263672d3..1893573c 100644 --- a/swarm-controller/src/matrix_account.rs +++ b/swarm-controller/src/matrix_account.rs @@ -42,8 +42,8 @@ fn default_mode() -> String { /// directly — see that fn's doc. /// /// Not yet consulted by [`put_matrix_account`]: `homeserver_or_configured_default` -/// below exists for a later slice of hyperhive#4345 to call; this one only -/// wires the config through. +/// below exists for a later slice of this homeserver-default rollout to +/// call; this one only wires the config through. pub(crate) const DEFAULT_HOMESERVER_ENV: &str = "SWARM_CONTROLLER_MATRIX_HOMESERVER_URL"; /// `caller`'s own homeserver, or `default` when the caller left it unset.