diff --git a/hive-c0re/src/matrix.rs b/hive-c0re/src/matrix.rs index dd20c3fd..ac317790 100644 --- a/hive-c0re/src/matrix.rs +++ b/hive-c0re/src/matrix.rs @@ -225,7 +225,15 @@ fn extract_access_token(body: &serde_json::Value) -> Result { /// homeserver. Skips registration entirely if the token file already /// exists (treating a present token as proof the account is good). /// To force re-registration, delete the token file. -pub async fn ensure_user_for(name: &str, register_token: &str) -> Result<()> { +/// +/// `client` is shared across the sweep so we build one reqwest +/// connection pool for all agents rather than one per call (argus +/// nit on #565 — bounded but wasteful). +pub async fn ensure_user_for( + client: &reqwest::Client, + name: &str, + register_token: &str, +) -> Result<()> { use std::os::unix::fs::PermissionsExt; let path = token_path(name); if path.exists() @@ -235,11 +243,7 @@ pub async fn ensure_user_for(name: &str, register_token: &str) -> Result<()> { tracing::debug!(%name, "matrix: token already present"); return Ok(()); } - let client = reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(HTTP_TIMEOUT_SECS)) - .build() - .context("matrix: build HTTP client")?; - let access_token = register_user(&client, name, register_token).await?; + let access_token = register_user(client, name, register_token).await?; if let Some(parent) = path.parent() { std::fs::create_dir_all(parent).ok(); } @@ -253,8 +257,8 @@ pub async fn ensure_user_for(name: &str, register_token: &str) -> Result<()> { /// Per-agent matrix sync: ensure the agent has a matrix account + token. /// All operations are idempotent; failures are logged as warnings but /// don't abort the caller. -pub async fn sync_agent(name: &str, register_token: &str) { - if let Err(e) = ensure_user_for(name, register_token).await { +pub async fn sync_agent(client: &reqwest::Client, name: &str, register_token: &str) { + if let Err(e) = ensure_user_for(client, name, register_token).await { tracing::warn!(%name, error = ?e, "matrix: ensure_user failed"); } } @@ -276,6 +280,18 @@ pub async fn ensure_all() { return; } }; + // One HTTP client for the whole sweep — connection pool is + // reused across agents. + let client = match reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(HTTP_TIMEOUT_SECS)) + .build() + { + Ok(c) => c, + Err(e) => { + tracing::warn!(error = ?e, "matrix: build HTTP client failed; skipping sweep"); + return; + } + }; let Ok(containers) = crate::lifecycle::list().await else { tracing::warn!("matrix: nixos-container list failed; skipping user sweep"); return; @@ -288,7 +304,7 @@ pub async fn ensure_all() { } else { continue; }; - sync_agent(&name, ®ister_token).await; + sync_agent(&client, &name, ®ister_token).await; } } diff --git a/nix/modules/hive-matrix.nix b/nix/modules/hive-matrix.nix index edb03f3c..2e03f51b 100644 --- a/nix/modules/hive-matrix.nix +++ b/nix/modules/hive-matrix.nix @@ -181,6 +181,25 @@ in } ]; + # Generate the registration token at system activation time, BEFORE + # the hive-matrix container would otherwise start with an empty + # bind-mount target (argus nit on #565: nspawn creates an empty + # file when the host path is missing, tuwunel reads it as + # `registration_token_file=""` and rejects every registration + # until the next restart). Idempotent: only writes when the file + # doesn't exist. 32-byte hex = 64 chars, same shape hive-c0re's + # `matrix::ensure_register_token` would produce. + system.activationScripts.hive-matrix-register-token = lib.stringAfter [ "var" ] '' + tokenFile=${lib.escapeShellArg (toString cfg.registrationTokenFile)} + if [ ! -s "$tokenFile" ]; then + mkdir -p "$(dirname "$tokenFile")" + head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' > "$tokenFile" + echo >> "$tokenFile" + chmod 600 "$tokenFile" + echo "hive-matrix: generated registration token at $tokenFile" + fi + ''; + containers.hive-matrix = { autoStart = true; ephemeral = false;