fix(#1185): move matrix-password outside purgeable agent_state_root

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/<name>-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
This commit is contained in:
atlas 2026-06-03 20:44:56 +02:00 committed by mara
commit 47e9c1cc1b

View file

@ -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/<name>-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 <pw>"
"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}:<server> \\\n\
-H 'Authorization: Bearer <admin-token>' \\\n\
-d '{{\"password\": \"<new-pw>\"}}'\n\
2. write the new password to {pw_path}:\n\
echo '<new-pw>' > {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(|| {