Watch
0
0
Fork
You've already forked hyperhive
0

hive-matrix-mcp: probe whoami through swarm-matrix-client

This commit is contained in:
atlas 2026-10-18 07:31:26 +02:00 • committed by mara
commit b17da10cfc
2 changed files with 67 additions and 95 deletions

View file

@ -723,7 +723,9 @@ fn forbidden_invite(membership: Option<&str>, user_id: &str, room_id: &str) -> R
tracing::debug!(%user_id, %room_id, "matrix: invite refused, user already member/invited"); tracing::debug!(%user_id, %room_id, "matrix: invite refused, user already member/invited");
return Ok(()); return Ok(());
} }
anyhow::bail!("matrix: invite {user_id} to {room_id}: HTTP 403 and the user is not invited or joined") anyhow::bail!(
"matrix: invite {user_id} to {room_id}: HTTP 403 and the user is not invited or joined"
)
} }
/// Invite an arbitrary Matrix user to the hive Space (default) or an /// Invite an arbitrary Matrix user to the hive Space (default) or an
@ -1044,7 +1046,6 @@ mod tests {
} }
} }
/// Serve a fake homeserver on loopback: `!space` is an `m.space` and /// Serve a fake homeserver on loopback: `!space` is an `m.space` and
/// `!chat` a plain room, both named "shared"; `!decoy` is a plain room /// `!chat` a plain room, both named "shared"; `!decoy` is a plain room
/// named "other". Returns the base URL and the server task. /// named "other". Returns the base URL and the server task.

View file

@ -36,7 +36,7 @@ use matrix_sdk::{
encryption::{BackupDownloadStrategy, EncryptionSettings}, encryption::{BackupDownloadStrategy, EncryptionSettings},
ruma::{OwnedDeviceId, OwnedUserId}, ruma::{OwnedDeviceId, OwnedUserId},
}; };
use serde::Deserialize; use swarm_matrix_client::WhoamiSession;
use tokio::fs; use tokio::fs;
use crate::credential::{Passphrase, Token}; use crate::credential::{Passphrase, Token};
@ -61,22 +61,12 @@ impl std::fmt::Display for PermanentBringUpError {
impl std::error::Error for PermanentBringUpError {} impl std::error::Error for PermanentBringUpError {}
/// Subset of the `/_matrix/client/v3/account/whoami` response we care
/// about. matrix-spec field names; `device_id` is optional per spec
/// (servers MAY omit it for legacy bearer scopes) but tuwunel always
/// returns it.
#[derive(Debug, Deserialize)]
struct WhoamiResponse {
user_id: String,
device_id: Option<String>,
}
/// Build + restore a matrix-sdk `Client` for the per-agent bearer /// Build + restore a matrix-sdk `Client` for the per-agent bearer
/// `token`. The Client points at `homeserver`, persists its sqlite cache /// `token`. The Client points at `homeserver`, persists its sqlite cache
/// under `state_dir`, and is ready for sync once returned. /// under `state_dir`, and is ready for sync once returned.
/// ///
/// Steps: /// Steps:
/// 1. Plain reqwest GET to `/_matrix/client/v3/account/whoami` with /// 1. A `swarm-matrix-client` whoami call with
/// the bearer — this gives us back the matrix `user_id` + /// the bearer — this gives us back the matrix `user_id` +
/// `device_id` (the registration response had them but only the /// `device_id` (the registration response had them but only the
/// token was kept; whoami is the cheapest recovery path /// token was kept; whoami is the cheapest recovery path
@ -95,11 +85,7 @@ pub async fn build_and_restore(
state_dir: &Path, state_dir: &Path,
is_primary: bool, is_primary: bool,
) -> Result<Client> { ) -> Result<Client> {
let (user_id, device_id) = match whoami(homeserver, token.expose()).await { let Some((user_id, device_id)) = whoami(homeserver, token.expose()).await? else {
Ok(ids) => ids,
Err(e) => {
let msg = format!("{e:#}");
if msg.contains("M_UNKNOWN_TOKEN") {
// Homeserver rejected our token — stale session after a homeserver // Homeserver rejected our token — stale session after a homeserver
// state wipe or token expiry. Discard it so we don't loop on it // 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 // (which for a store-held credential is a log line and nothing
@ -143,11 +129,9 @@ pub async fn build_and_restore(
"matrix token rejected (M_UNKNOWN_TOKEN); discarded stale token" "matrix token rejected (M_UNKNOWN_TOKEN); discarded stale token"
); );
return Err(anyhow::Error::new(PermanentBringUpError( return Err(anyhow::Error::new(PermanentBringUpError(
"matrix token rejected (M_UNKNOWN_TOKEN); discarded stale token, leaving account down".into(), "matrix token rejected (M_UNKNOWN_TOKEN); discarded stale token, leaving account down"
.into(),
))); )));
}
return Err(e);
}
}; };
fs::create_dir_all(state_dir) fs::create_dir_all(state_dir)
@ -182,38 +166,25 @@ pub async fn build_and_restore(
Ok(client) Ok(client)
} }
/// Bare-reqwest whoami probe — used at startup to recover the /// Whoami probe — used at startup to recover the `user_id` + `device_id` the
/// `user_id` + `device_id` the registration response carried but /// registration response carried but hive-c0re didn't persist alongside the
/// hive-c0re didn't persist alongside the access token. Cheaper than /// access token. Cheaper than teaching the hive-c0re side to persist them,
/// teaching the hive-c0re side to persist them, plus matches what a /// plus matches what a fresh deployment with a hand-rolled token
/// fresh deployment with a hand-rolled token (`HIVE_MATRIX_TOKEN_FILE` /// (`HIVE_MATRIX_TOKEN_FILE` pointing somewhere unexpected) needs anyway.
/// pointing somewhere unexpected) needs anyway. ///
async fn whoami(homeserver: &str, token: &str) -> Result<(OwnedUserId, OwnedDeviceId)> { /// `None` is the homeserver's `M_UNKNOWN_TOKEN` verdict on the token.
let url = format!("{homeserver}/_matrix/client/v3/account/whoami"); async fn whoami(homeserver: &str, token: &str) -> Result<Option<(OwnedUserId, OwnedDeviceId)>> {
let resp = reqwest::Client::builder() let http = swarm_matrix_client::client()?;
.timeout(std::time::Duration::from_secs(10)) let (user_id, device_id) =
.build() match swarm_matrix_client::whoami_session(&http, homeserver, token).await? {
.context("build whoami reqwest client")? WhoamiSession::UnknownToken => return Ok(None),
.get(&url) WhoamiSession::Session { user_id, device_id } => (user_id, device_id),
.bearer_auth(token) };
.send() let parsed: OwnedUserId = user_id
.await
.with_context(|| format!("GET {url}"))?;
let status = resp.status();
if !status.is_success() {
let body = resp.text().await.unwrap_or_default();
return Err(anyhow!("whoami GET {url} → HTTP {status}, body: {body}"));
}
let body: WhoamiResponse = resp.json().await.context("parse whoami response")?;
let user_id: OwnedUserId = body
.user_id
.parse() .parse()
.with_context(|| format!("invalid user_id in whoami: {}", body.user_id))?; .with_context(|| format!("invalid user_id in whoami: {user_id}"))?;
let device_id_raw = body let device_id = device_id.ok_or_else(|| anyhow!("whoami response missing device_id"))?;
.device_id Ok(Some((parsed, device_id.into())))
.ok_or_else(|| anyhow!("whoami response missing device_id"))?;
let device_id: OwnedDeviceId = device_id_raw.into();
Ok((user_id, device_id))
} }
/// Best-effort: bootstrap this account's cross-signing identity so its /// Best-effort: bootstrap this account's cross-signing identity so its