From 47e9c1cc1baf7344931df4c5786507e5ac114c74 Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 3 Jun 2026 20:44:56 +0200 Subject: [PATCH] fix(#1185): move matrix-password outside purgeable agent_state_root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The password file was stored at agent_notes_dir/matrix-password which lives inside agent_state_root — wiped by destroy --purge. On re-spawn with the same agent name, the matrix user still exists in the homeserver but the stored password is gone, making re-login impossible. Move password to /var/lib/hyperhive/matrix-creds/-password which is not deleted by purge. On re-spawn, ensure_user_for finds M_USER_IN_USE, reads the preserved password, re-logins, and writes a fresh token. Also: - add one-time migration that moves existing passwords from the old path to the new location on first access after upgrade - remove chown_to_agent on the password file (it is now host-only, not inside the agent bind-mount tree) - fix the error message to give actionable recovery steps instead of suggesting hivectl matrix create-user --password which is rejected for agent accounts --- hive-c0re/src/matrix.rs | 59 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/hive-c0re/src/matrix.rs b/hive-c0re/src/matrix.rs index fb7a349a..9a18876d 100644 --- a/hive-c0re/src/matrix.rs +++ b/hive-c0re/src/matrix.rs @@ -45,10 +45,27 @@ fn token_path(name: &str) -> PathBuf { Coordinator::agent_notes_dir(name).join("matrix-token") } -/// Password file alongside the token. Persisted so we can fall back to -/// `m.login.password` if the token file is deleted but the homeserver -/// account still exists. Mode 0600, same dir as the token. +/// 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. +/// +/// Path: `/var/lib/hyperhive/matrix-creds/-password` +/// +/// The token file lives inside the agent's bind-mounted state dir (under +/// `agent_notes_dir`) so the agent container can read it; the password +/// file is host-side only (agents never log in by password — they use +/// the access token exclusively) and belongs with other hive-c0re +/// credential state, not inside the purgeable per-agent tree. fn password_path(name: &str) -> PathBuf { + PathBuf::from("/var/lib/hyperhive/matrix-creds").join(format!("{name}-password")) +} + +/// Legacy password path (inside the old purgeable `agent_notes_dir`). +/// Used only during the one-time migration in [`ensure_user_for`] to +/// move credentials from old deployments to the new location. Safe to +/// call after `destroy --purge` — the path will simply not exist and +/// the migration is a no-op. +fn legacy_password_path(name: &str) -> PathBuf { Coordinator::agent_notes_dir(name).join("matrix-password") } @@ -276,6 +293,29 @@ pub async fn ensure_user_for( return Ok(()); } + // One-time migration: move the password from the old location inside + // agent_notes_dir (purgeable) to the new location outside it. + let new_pw_path = password_path(name); + let old_pw_path = legacy_password_path(name); + if !new_pw_path.exists() && old_pw_path.exists() { + if let Some(parent) = new_pw_path.parent() { + std::fs::create_dir_all(parent).ok(); + } + if let Err(e) = std::fs::rename(&old_pw_path, &new_pw_path) { + // Rename across filesystems or read-only src — copy + delete. + if let Ok(content) = std::fs::read(&old_pw_path) { + if std::fs::write(&new_pw_path, &content).is_ok() { + let _ = std::fs::remove_file(&old_pw_path); + tracing::info!(%name, "matrix: migrated password file to non-purgeable location"); + } + } else { + tracing::warn!(%name, error = ?e, "matrix: password migration failed (old path stays)"); + } + } else { + tracing::info!(%name, "matrix: migrated password file to non-purgeable location"); + } + } + let password = random_password()?; let access_token = match register_user(client, name, register_token, &password).await { Ok(token) => { @@ -289,7 +329,6 @@ pub async fn ensure_user_for( tracing::warn!(%name, error = ?e, "matrix: failed to persist password (token still saved)"); } else { let _ = std::fs::set_permissions(&pw_path, std::fs::Permissions::from_mode(0o600)); - crate::lifecycle::chown_to_agent(name, &pw_path, "matrix"); } token } @@ -303,8 +342,16 @@ pub async fn ensure_user_for( .filter(|s| !s.is_empty()) .with_context(|| { format!( - "matrix: user {name} already exists in homeserver but matrix-password \ - is missing — manual recovery: hivectl matrix create-user {name} --password " + "matrix: user {name} already exists in homeserver but the stored \ + password is missing — manual recovery:\n\ + 1. reset the password via the matrix admin API:\n\ + curl -X PUT http://localhost:8008/_synapse/admin/v2/users/@{name}: \\\n\ + -H 'Authorization: Bearer ' \\\n\ + -d '{{\"password\": \"\"}}'\n\ + 2. write the new password to {pw_path}:\n\ + echo '' > {pw_path} && chmod 600 {pw_path}\n\ + 3. run: hivectl matrix create-user {name}", + pw_path = pw_path.display() ) })?; login_user(client, name, &stored).await.with_context(|| {