From 46d0b466703811a0defc3ee80d0bea1bbaf2e899 Mon Sep 17 00:00:00 2001 From: atlas Date: Thu, 24 Sep 2026 11:01:03 +0200 Subject: [PATCH] hive-c0re: decide matrix token presence from metadata, not by reading it hive-priv writes an agent's matrix-token 0600 and owned by the agent, so hive-c0re, running as hive-core, cannot read it. The read-based token-present guard in ensure_user_for therefore never fired, and every matrix sweep (boot, every 30 minutes, every rebuild) re-minted each agent's token through the appservice login and restarted its hive-matrix-daemon. Decide presence with a stat instead: a non-empty regular file counts as present. hive-core can stat the file through the 0755 state dir. Closes #4665 --- hive-c0re/src/matrix.rs | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/hive-c0re/src/matrix.rs b/hive-c0re/src/matrix.rs index e7f7c433..224ea86a 100644 --- a/hive-c0re/src/matrix.rs +++ b/hive-c0re/src/matrix.rs @@ -132,6 +132,15 @@ fn token_path(name: &hive_types::Ident) -> PathBuf { Coordinator::agent_notes_dir(name).join("matrix-token") } +/// Whether an agent's token file is present: a non-empty regular file. +/// Decided from metadata alone, because hive-priv writes the file 0600 +/// and owned by the agent, so `hive-core` cannot read it but can stat it +/// through the 0755 state dir. A check that reads the file always +/// fails here and makes every sweep re-mint the token. +fn token_file_present(path: &std::path::Path) -> bool { + std::fs::metadata(path).is_ok_and(|m| m.is_file() && m.len() > 0) +} + /// Password file for the agent's matrix account. Stored OUTSIDE the /// purgeable `agent_state_root` tree so it survives `destroy --purge` /// and allows re-login recovery when the same agent name is re-spawned. @@ -719,11 +728,7 @@ pub async fn ensure_user_for(client: &reqwest::Client, name: &str, as_token: &st use std::os::unix::fs::PermissionsExt; let agent = hive_types::Ident::parse(name) .map_err(|e| anyhow::anyhow!("invalid agent name {name:?}: {e}"))?; - let path = token_path(&agent); - if path.exists() - && let Ok(existing) = std::fs::read_to_string(&path) - && !existing.trim().is_empty() - { + if token_file_present(&token_path(&agent)) { tracing::debug!(%name, "matrix: token already present"); return Ok(()); } @@ -2096,4 +2101,30 @@ mod tests { let err = extract_access_token(&body).unwrap_err(); assert!(err.to_string().contains("missing access_token")); } + + #[test] + fn token_file_present_only_for_a_non_empty_regular_file() { + let dir = tempfile::tempdir().expect("tempdir"); + let token = dir.path().join("matrix-token"); + assert!(!token_file_present(&token), "missing file"); + std::fs::write(&token, "").unwrap(); + assert!(!token_file_present(&token), "empty file"); + std::fs::write(&token, "tok\n").unwrap(); + assert!(token_file_present(&token), "non-empty file"); + assert!(!token_file_present(dir.path()), "directory"); + } + + /// The sweep runs as `hive-core`, which cannot read the agent-owned + /// 0600 token file. `chmod 000` does not stop root, so this test uses + /// content that `read_to_string` rejects instead: the predicate must + /// still report the file as present, which holds only if it never + /// reads the content. + #[test] + fn token_file_present_does_not_read_the_content() { + let dir = tempfile::tempdir().expect("tempdir"); + let token = dir.path().join("matrix-token"); + std::fs::write(&token, [0xff, 0xfe]).unwrap(); + assert!(std::fs::read_to_string(&token).is_err()); + assert!(token_file_present(&token)); + } }