From 78317b9cacc6dfadc2714a839a5ea1765a56eced Mon Sep 17 00:00:00 2001 From: damocles Date: Wed, 3 Jun 2026 23:26:26 +0200 Subject: [PATCH] feat(#551): switch read_room to room.messages() for e2ee decryption, add UTD sentinel, update docs --- docs/matrix.md | 11 +++++-- hive-matrix-mcp/src/handlers.rs | 51 ++++++++++++++++++++++++--------- 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/docs/matrix.md b/docs/matrix.md index 67cb5f5f..b4f95f23 100644 --- a/docs/matrix.md +++ b/docs/matrix.md @@ -119,8 +119,15 @@ Initial rollout settings: engage). The absent `yes_i_am_very_very_sure_…_open_registration_…` flag keeps the server closed to anyone without the token. -- `allow_encryption = false` per operator call; E2EE re-enabling is - deferred to a follow-up. +- `allow_encryption = true` — E2EE enabled. Agent clients use the + `e2e-encryption` feature of `matrix-sdk`; crypto keys are stored in + the per-agent sqlite store under the state dir and persist across + restarts (lost on `--purge`). `read_room` decrypts via + `room.messages()` — UTD events surface as `event_type = + "m.room.encrypted"` with `body = "[unable to decrypt]"`. + Cross-signing and automatic key backup are not enabled for the first + pass: static bearer-token bot accounts can't bootstrap cross-signing + without MSC3967. ## Hive Matrix Space diff --git a/hive-matrix-mcp/src/handlers.rs b/hive-matrix-mcp/src/handlers.rs index 23125db7..cd38835b 100644 --- a/hive-matrix-mcp/src/handlers.rs +++ b/hive-matrix-mcp/src/handlers.rs @@ -116,6 +116,24 @@ fn extract_body(event: &matrix_sdk::ruma::events::AnyTimelineEvent) -> String { } } +/// Same as [`extract_body`] but for the `AnySyncTimelineEvent` variant +/// returned by `TimelineEvent::raw()` (the sdk-level decrypted form from +/// `room.messages()`). +fn extract_body_sync(event: &matrix_sdk::ruma::events::AnySyncTimelineEvent) -> String { + use matrix_sdk::ruma::events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent}; + match event { + AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(ev)) => ev + .as_original() + .map_or_else(String::new, |orig| match &orig.content.msgtype { + MessageType::Text(t) => t.body.clone(), + MessageType::Notice(n) => n.body.clone(), + MessageType::Emote(e) => format!("* {}", e.body), + _ => String::new(), + }), + _ => String::new(), + } +} + pub async fn send_message(client: &Client, room_ref: &str, body: &str) -> DaemonResponse { let room = match resolve_room(client, room_ref).await { Ok(r) => r, @@ -408,32 +426,37 @@ pub async fn invite_user(client: &Client, room_ref: &str, user_id: &str) -> Daem } pub async fn read_room(client: &Client, room_ref: &str, limit: Option) -> DaemonResponse { - use matrix_sdk::ruma::api::Direction; - use matrix_sdk::ruma::api::client::message::get_message_events; + use matrix_sdk::room::MessagesOptions; let room = match resolve_room(client, room_ref).await { Ok(r) => r, Err(e) => return e, }; - let limit = limit.unwrap_or(50).min(200); - let mut req = - get_message_events::v3::Request::new(room.room_id().to_owned(), Direction::Backward); - req.limit = matrix_sdk::ruma::UInt::try_from(limit as u64) - .unwrap_or(matrix_sdk::ruma::UInt::from(50u32)); - let resp = match client.send(req).await { - Ok(r) => r, - Err(e) => return DaemonResponse::error(format!("get_message_events: {e}")), + let limit_val = u32::try_from(limit.unwrap_or(50).min(200)).unwrap_or(50); + let mut opts = MessagesOptions::backward(); + opts.limit = matrix_sdk::ruma::UInt::from(limit_val); + // room.messages() transparently decrypts events in encrypted rooms. + // UTD (unable to decrypt) events surface via `ev.kind.is_utd()` and + // get a sentinel body so claude knows decryption failed rather than + // seeing the raw encrypted blob. + let msgs = match room.messages(opts).await { + Ok(m) => m, + Err(e) => return DaemonResponse::error(format!("messages: {e}")), }; - let events: Vec = resp + let events: Vec = msgs .chunk .iter() - .filter_map(|raw| { - let parsed = raw.deserialize().ok()?; + .filter_map(|ev| { + let parsed = ev.raw().deserialize().ok()?; let event_id = parsed.event_id().to_string(); let sender = parsed.sender().to_string(); let origin_server_ts: i64 = parsed.origin_server_ts().0.into(); let event_type = parsed.event_type().to_string(); - let body = extract_body(&parsed); + let body = if ev.kind.is_utd() { + "[unable to decrypt]".to_owned() + } else { + extract_body_sync(&parsed) + }; Some(TimelineEvent { event_id, sender,