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");
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
@ -1044,7 +1046,6 @@ mod tests {
}
}
/// Serve a fake homeserver on loopback: `!space` is an `m.space` and
/// `!chat` a plain room, both named "shared"; `!decoy` is a plain room
/// named "other". Returns the base URL and the server task.

View file

@ -36,7 +36,7 @@ use matrix_sdk::{
encryption::{BackupDownloadStrategy, EncryptionSettings},
ruma::{OwnedDeviceId, OwnedUserId},
};
use serde::Deserialize;
use swarm_matrix_client::WhoamiSession;
use tokio::fs;
use crate::credential::{Passphrase, Token};
@ -61,22 +61,12 @@ impl std::fmt::Display 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
/// `token`. The Client points at `homeserver`, persists its sqlite cache
/// under `state_dir`, and is ready for sync once returned.
///
/// 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` +
/// `device_id` (the registration response had them but only the
/// token was kept; whoami is the cheapest recovery path
@ -95,11 +85,7 @@ pub async fn build_and_restore(
state_dir: &Path,
is_primary: bool,
) -> Result<Client> {
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") {
let Some((user_id, device_id)) = whoami(homeserver, token.expose()).await? else {
// Homeserver rejected our token — stale session after a homeserver
// 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
@ -143,11 +129,9 @@ pub async fn build_and_restore(
"matrix token rejected (M_UNKNOWN_TOKEN); discarded stale token"
);
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)
@ -182,38 +166,25 @@ pub async fn build_and_restore(
Ok(client)
}
/// Bare-reqwest whoami probe — used at startup to recover the
/// `user_id` + `device_id` the registration response carried but
/// hive-c0re didn't persist alongside the access token. Cheaper than
/// teaching the hive-c0re side to persist them, plus matches what a
/// fresh deployment with a hand-rolled token (`HIVE_MATRIX_TOKEN_FILE`
/// pointing somewhere unexpected) needs anyway.
async fn whoami(homeserver: &str, token: &str) -> Result<(OwnedUserId, OwnedDeviceId)> {
let url = format!("{homeserver}/_matrix/client/v3/account/whoami");
let resp = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.context("build whoami reqwest client")?
.get(&url)
.bearer_auth(token)
.send()
.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
/// Whoami probe — used at startup to recover the `user_id` + `device_id` the
/// registration response carried but hive-c0re didn't persist alongside the
/// access token. Cheaper than teaching the hive-c0re side to persist them,
/// plus matches what a fresh deployment with a hand-rolled token
/// (`HIVE_MATRIX_TOKEN_FILE` pointing somewhere unexpected) needs anyway.
///
/// `None` is the homeserver's `M_UNKNOWN_TOKEN` verdict on the token.
async fn whoami(homeserver: &str, token: &str) -> Result<Option<(OwnedUserId, OwnedDeviceId)>> {
let http = swarm_matrix_client::client()?;
let (user_id, device_id) =
match swarm_matrix_client::whoami_session(&http, homeserver, token).await? {
WhoamiSession::UnknownToken => return Ok(None),
WhoamiSession::Session { user_id, device_id } => (user_id, device_id),
};
let parsed: OwnedUserId = user_id
.parse()
.with_context(|| format!("invalid user_id in whoami: {}", body.user_id))?;
let device_id_raw = body
.device_id
.ok_or_else(|| anyhow!("whoami response missing device_id"))?;
let device_id: OwnedDeviceId = device_id_raw.into();
Ok((user_id, device_id))
.with_context(|| format!("invalid user_id in whoami: {user_id}"))?;
let device_id = device_id.ok_or_else(|| anyhow!("whoami response missing device_id"))?;
Ok(Some((parsed, device_id.into())))
}
/// Best-effort: bootstrap this account's cross-signing identity so its