fix(#922): fall back to m.login.password when matrix user already exists
When the token file is deleted but the homeserver account still exists, register_user returns M_USER_IN_USE (HTTP 400) and the provisioning sweep hard-fails, leaving the agent without a working matrix token. Fix: persist the random password to matrix-password alongside the access token on first registration. On subsequent attempts where M_USER_IN_USE is returned, fall back to login_user (m.login.password) using the stored password. If both files are gone, the error message guides the operator to `hivectl matrix create-user <name> --password`.
This commit is contained in:
parent
ee61eac663
commit
e3b4d38565
1 changed files with 92 additions and 4 deletions
|
|
@ -46,6 +46,13 @@ fn token_path(name: &str) -> PathBuf {
|
||||||
Coordinator::agent_notes_dir(name).join("matrix-token")
|
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.
|
||||||
|
fn password_path(name: &str) -> PathBuf {
|
||||||
|
Coordinator::agent_notes_dir(name).join("matrix-password")
|
||||||
|
}
|
||||||
|
|
||||||
/// Probe whether `hive-matrix` exists as a nixos-container. Cheap —
|
/// Probe whether `hive-matrix` exists as a nixos-container. Cheap —
|
||||||
/// `nixos-container list` is just a directory scan in /etc. Same shape
|
/// `nixos-container list` is just a directory scan in /etc. Same shape
|
||||||
/// as `forge::is_present`.
|
/// as `forge::is_present`.
|
||||||
|
|
@ -211,10 +218,51 @@ fn extract_access_token(body: &serde_json::Value) -> Result<String> {
|
||||||
.with_context(|| format!("matrix: missing access_token in response: {body}"))
|
.with_context(|| format!("matrix: missing access_token in response: {body}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Login with `m.login.password` and return the access token. Fallback
|
||||||
|
/// for when registration fails with `M_USER_IN_USE` — the account
|
||||||
|
/// already exists in the homeserver but the token file was lost. Fails
|
||||||
|
/// if the stored password no longer matches (e.g. homeserver wiped),
|
||||||
|
/// in which case manual recovery via `hivectl matrix create-user` is
|
||||||
|
/// required.
|
||||||
|
async fn login_user(client: &reqwest::Client, agent: &str, password: &str) -> Result<String> {
|
||||||
|
let url = format!("{MATRIX_HTTP}/_matrix/client/v3/login");
|
||||||
|
let body = serde_json::json!({
|
||||||
|
"type": "m.login.password",
|
||||||
|
"identifier": {
|
||||||
|
"type": "m.id.user",
|
||||||
|
"user": user_localpart(agent),
|
||||||
|
},
|
||||||
|
"password": password,
|
||||||
|
"device_id": format!("hyperhive-{agent}"),
|
||||||
|
"initial_device_display_name": format!("hyperhive ({agent})"),
|
||||||
|
});
|
||||||
|
let resp = client
|
||||||
|
.post(&url)
|
||||||
|
.json(&body)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("matrix: POST /login")?;
|
||||||
|
let status = resp.status();
|
||||||
|
let json = resp
|
||||||
|
.json::<serde_json::Value>()
|
||||||
|
.await
|
||||||
|
.context("matrix: parse /login response")?;
|
||||||
|
if !status.is_success() {
|
||||||
|
anyhow::bail!("matrix: /login HTTP {status} for agent {agent}, body: {json}");
|
||||||
|
}
|
||||||
|
extract_access_token(&json)
|
||||||
|
}
|
||||||
|
|
||||||
/// Ensure `name` has a matrix user + token file on the local
|
/// Ensure `name` has a matrix user + token file on the local
|
||||||
/// homeserver. Skips registration entirely if the token file already
|
/// homeserver. Skips provisioning entirely if the token file already
|
||||||
/// exists (treating a present token as proof the account is good).
|
/// exists (treating a present token as proof the account is good).
|
||||||
/// To force re-registration, delete the token file.
|
/// To force re-provisioning, delete the token file.
|
||||||
|
///
|
||||||
|
/// When registration fails with `M_USER_IN_USE` (account exists in the
|
||||||
|
/// homeserver but the token file was deleted) this falls back to
|
||||||
|
/// `m.login.password` using the persisted `matrix-password` file. If
|
||||||
|
/// that file is also missing, recovery requires manual intervention:
|
||||||
|
/// `hivectl matrix create-user <name> --password <pw>`.
|
||||||
///
|
///
|
||||||
/// `client` is shared across the sweep so we build one reqwest
|
/// `client` is shared across the sweep so we build one reqwest
|
||||||
/// connection pool for all agents rather than one per call.
|
/// connection pool for all agents rather than one per call.
|
||||||
|
|
@ -232,8 +280,48 @@ pub async fn ensure_user_for(
|
||||||
tracing::debug!(%name, "matrix: token already present");
|
tracing::debug!(%name, "matrix: token already present");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let password = random_password()?;
|
let password = random_password()?;
|
||||||
let access_token = register_user(client, name, register_token, &password).await?;
|
let access_token = match register_user(client, name, register_token, &password).await {
|
||||||
|
Ok(token) => {
|
||||||
|
// Successful registration — persist the password so we can
|
||||||
|
// fall back to login if the token file is deleted later.
|
||||||
|
let pw_path = password_path(name);
|
||||||
|
if let Some(parent) = pw_path.parent() {
|
||||||
|
std::fs::create_dir_all(parent).ok();
|
||||||
|
}
|
||||||
|
if let Err(e) = std::fs::write(&pw_path, format!("{password}\n")) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
Err(reg_err) if reg_err.to_string().contains("M_USER_IN_USE") => {
|
||||||
|
// Account already exists — try to re-login with the stored password.
|
||||||
|
tracing::info!(%name, "matrix: user already exists, attempting login with stored password");
|
||||||
|
let pw_path = password_path(name);
|
||||||
|
let stored = std::fs::read_to_string(&pw_path)
|
||||||
|
.ok()
|
||||||
|
.map(|s| s.trim().to_owned())
|
||||||
|
.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>"
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
login_user(client, name, &stored).await.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"matrix: login fallback for {name} failed — if homeserver was wiped, delete \
|
||||||
|
the matrix-password file and retry"
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
Err(other) => return Err(other),
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
std::fs::create_dir_all(parent).ok();
|
std::fs::create_dir_all(parent).ok();
|
||||||
}
|
}
|
||||||
|
|
@ -241,7 +329,7 @@ pub async fn ensure_user_for(
|
||||||
.with_context(|| format!("matrix: write token to {}", path.display()))?;
|
.with_context(|| format!("matrix: write token to {}", path.display()))?;
|
||||||
let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600));
|
let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600));
|
||||||
crate::lifecycle::chown_to_agent(name, &path, "matrix");
|
crate::lifecycle::chown_to_agent(name, &path, "matrix");
|
||||||
tracing::info!(%name, path = %path.display(), "matrix: registered user + persisted access token");
|
tracing::info!(%name, path = %path.display(), "matrix: provisioned access token");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue