hive-matrix-mcp: read the main account's token from the store too
The daemon reads each account's token from `swarm/agents/<agent>/matrix/` as the agent itself, inside its own container, and falls back to the file only when the store has none. This is #4519's read, without its `main` carve-out: the swarm now mints `main` there and no hive writes the file. The daemon unit gets the agent's store identity, spelled the way forge-token.nix spells it. A timer re-starts it while it is down: a token the swarm mints or replaces in the store changes no file, so the path watcher never fires for it, and a daemon that exited on a replaced token would otherwise stay down until the container restarts.
This commit is contained in:
parent
2776e121e5
commit
ab153bda2f
9 changed files with 670 additions and 85 deletions
|
|
@ -1,13 +1,15 @@
|
|||
//! matrix-sdk `Client` setup for the daemon: read the per-agent access
|
||||
//! token from the state-dir file `hive-c0re::matrix::ensure_user_for`
|
||||
//! wrote, probe `whoami` to recover the agent's matrix `user_id` +
|
||||
//! `device_id`, restore the matrix-sdk session, return the Client ready
|
||||
//! to start sync.
|
||||
//! matrix-sdk `Client` setup for the daemon: take the per-agent access
|
||||
//! token [`crate::credential`] resolved, probe `whoami` to recover the
|
||||
//! agent's matrix `user_id` + `device_id`, restore the matrix-sdk
|
||||
//! session, return the Client ready to start sync.
|
||||
//!
|
||||
//! 🩸 The token arrives as a [`Token`] and never as a path this module
|
||||
//! reads — where it came from is [`crate::credential`]'s question, and the
|
||||
//! only thing said about it here is its [`crate::credential::Origin`].
|
||||
//!
|
||||
//! No OAuth dance / cross-signing setup (in contrast to damocles-daemon's
|
||||
//! ccc.de connection): for the in-hive tuwunel hive-c0re already minted
|
||||
//! the token + user/device as the hive's appservice and handed us the
|
||||
//! bearer in a file. matrix-sdk's `restore_session` with a constructed
|
||||
//! ccc.de connection): for the swarm's tuwunel the swarm already minted the
|
||||
//! token + user/device as its appservice and handed us the bearer. matrix-sdk's `restore_session` with a constructed
|
||||
//! `MatrixSession` skips the login flow entirely.
|
||||
//!
|
||||
//! E2EE is enabled via `with_encryption_settings(EncryptionSettings::default())`.
|
||||
|
|
@ -34,10 +36,12 @@ use matrix_sdk::{
|
|||
use serde::Deserialize;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::credential::Token;
|
||||
|
||||
/// Sentinel returned when `build_and_restore` detects that the token is
|
||||
/// permanently invalid (`M_UNKNOWN_TOKEN`). The token has already been
|
||||
/// removed from disk. Callers should NOT retry — the account needs
|
||||
/// re-provisioning by hive-c0re.
|
||||
/// discarded ([`Token::discard_stale`]). Callers should NOT retry — the
|
||||
/// account needs re-provisioning.
|
||||
///
|
||||
/// Distinct from the general `anyhow::Error` path so callers can use
|
||||
/// `err.downcast_ref::<PermanentBringUpError>()` to distinguish "retry
|
||||
|
|
@ -64,63 +68,53 @@ struct WhoamiResponse {
|
|||
}
|
||||
|
||||
/// Build + restore a matrix-sdk `Client` for the per-agent bearer
|
||||
/// token at `token_file`. The Client points at `homeserver`, persists
|
||||
/// its sqlite cache under `state_dir`, and is ready for sync once
|
||||
/// returned.
|
||||
/// `token`. The Client points at `homeserver`, persists its sqlite cache
|
||||
/// under `state_dir`, and is ready for sync once returned.
|
||||
///
|
||||
/// Steps:
|
||||
/// 1. Read the bearer token from `token_file` (trim trailing whitespace).
|
||||
/// 2. Plain reqwest GET to `/_matrix/client/v3/account/whoami` with
|
||||
/// 1. Plain reqwest GET to `/_matrix/client/v3/account/whoami` with
|
||||
/// the bearer — this gives us back the matrix `user_id` +
|
||||
/// `device_id` (the registration response had them but hive-c0re
|
||||
/// only persisted the token; whoami is the cheapest recovery path
|
||||
/// `device_id` (the registration response had them but only the
|
||||
/// token was kept; whoami is the cheapest recovery path
|
||||
/// and avoids matrix-sdk's circular requirement of needing a
|
||||
/// session to call whoami).
|
||||
/// 3. Build the real Client with the sqlite store + `restore_session`
|
||||
/// 2. Build the real Client with the sqlite store + `restore_session`
|
||||
/// using a synthetic `MatrixSession`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the token file is missing or empty, if the
|
||||
/// `whoami` request fails, or if the matrix-sdk client fails to build
|
||||
/// or restore the session.
|
||||
/// Returns an error if the `whoami` request fails, or if the matrix-sdk
|
||||
/// client fails to build or restore the session.
|
||||
pub async fn build_and_restore(
|
||||
homeserver: &str,
|
||||
token_file: &Path,
|
||||
token: &Token,
|
||||
state_dir: &Path,
|
||||
is_primary: bool,
|
||||
) -> Result<Client> {
|
||||
let token = fs::read_to_string(token_file)
|
||||
.await
|
||||
.with_context(|| format!("read matrix token from {}", token_file.display()))?
|
||||
.trim()
|
||||
.to_owned();
|
||||
if token.is_empty() {
|
||||
return Err(anyhow!("matrix token at {} is empty", token_file.display()));
|
||||
}
|
||||
|
||||
let (user_id, device_id) = match whoami(homeserver, &token).await {
|
||||
let (user_id, device_id) = match whoami(homeserver, token.expose()).await {
|
||||
Ok(ids) => ids,
|
||||
Err(e) => {
|
||||
let msg = format!("{e:#}");
|
||||
if msg.contains("M_UNKNOWN_TOKEN") {
|
||||
// Homeserver rejected our token — stale session after a homeserver
|
||||
// state wipe or token expiry. Delete the stale token file so we
|
||||
// don't loop on it. The rest of the handling depends on whether
|
||||
// this is the primary (hive-internal) account or a secondary one,
|
||||
// because a single bad secondary token must NOT take down the
|
||||
// whole daemon (and with it every healthy account).
|
||||
// state wipe or token expiry. Discard it so we don't loop on it
|
||||
// (which for a store-held credential is a log line and nothing
|
||||
// else — see `Token::discard_stale`). The rest of the handling
|
||||
// depends on whether this is the primary (hive-internal) account
|
||||
// or a secondary one, because a single bad secondary token must
|
||||
// NOT take down the whole daemon (and with it every healthy
|
||||
// account).
|
||||
tracing::warn!(
|
||||
path = %token_file.display(),
|
||||
credential = %token.origin(),
|
||||
is_primary,
|
||||
"matrix token rejected (M_UNKNOWN_TOKEN); removing stale token"
|
||||
"matrix token rejected (M_UNKNOWN_TOKEN); discarding stale token"
|
||||
);
|
||||
let _ = fs::remove_file(token_file).await;
|
||||
token.discard_stale().await;
|
||||
if is_primary {
|
||||
// Primary: also drop the matrix-sdk sqlite state keyed to the
|
||||
// now-invalid session, then exit 0 so hive-c0re's `ensure_all`
|
||||
// re-provisions the account and the systemd.paths watcher
|
||||
// restarts us once the fresh token file appears. Exit 0 (not
|
||||
// now-invalid session, then exit 0; the swarm's backfill
|
||||
// re-mints the token and systemd restarts us once it
|
||||
// exists (store re-check timer, or the path watcher). Exit 0 (not
|
||||
// Err) keeps systemd's Restart=on-failure from looping; the
|
||||
// call site is before any tasks are spawned so there's
|
||||
// nothing to clean up.
|
||||
|
|
@ -133,13 +127,13 @@ pub async fn build_and_restore(
|
|||
}
|
||||
std::process::exit(0);
|
||||
}
|
||||
// Secondary: token removed (so it's cleanly skipped next boot
|
||||
// Secondary: token discarded (so it's cleanly skipped next boot
|
||||
// rather than re-erroring); leave the sdk state in place in case
|
||||
// the operator re-provisions a fresh token for the same device.
|
||||
// Return a PermanentBringUpError so the caller can distinguish
|
||||
// "don't retry" from a transient network/DNS failure.
|
||||
return Err(anyhow::Error::new(PermanentBringUpError(
|
||||
"matrix token rejected (M_UNKNOWN_TOKEN); removed stale token, skipping account".into(),
|
||||
"matrix token rejected (M_UNKNOWN_TOKEN); discarded stale token, skipping account".into(),
|
||||
)));
|
||||
}
|
||||
return Err(e);
|
||||
|
|
@ -159,7 +153,7 @@ pub async fn build_and_restore(
|
|||
let session = MatrixSession {
|
||||
meta: SessionMeta { user_id, device_id },
|
||||
tokens: SessionTokens {
|
||||
access_token: token,
|
||||
access_token: token.expose().to_owned(),
|
||||
refresh_token: None,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue