From ff256d0b1c6a666103a7182e60157c31747a05f9 Mon Sep 17 00:00:00 2001 From: atlas Date: Sun, 18 Oct 2026 09:27:09 +0200 Subject: [PATCH] swarm-matrix-client: document the room calls; test the whoami refusal --- AGENTS.md | 5 +++-- hive-matrix-mcp/src/client.rs | 37 ++++++++++++++++++++++++++++++++++ swarm-matrix-client/README.md | 10 +++++++-- swarm-matrix-client/src/lib.rs | 16 +++++++++------ 4 files changed, 58 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e6b9f51d..aea39123 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -152,8 +152,9 @@ Swarm-level: bearer on the metrics store's query API. - **`swarm-logs/`** — agent CLI: a LogsQL query in, log lines out. - **`swarm-metrics/`** — agent CLI: a MetricsQL query in, the store's JSON out. -- **`swarm-matrix-client/`** — appservice calls against the swarm - homeserver, shared by `swarm-controller` and `swarm-matrix-ctl`. +- **`swarm-matrix-client/`** — client-server API calls against the swarm + homeserver, shared by `swarm-controller`, `swarm-matrix-ctl`, `hive-c0re` + and `hive-matrix-mcp`. - **`swarm-matrix-ctl/`** — the one binary run inside `containers.hive-matrix`; new in-container jobs are verbs here, not crates. diff --git a/hive-matrix-mcp/src/client.rs b/hive-matrix-mcp/src/client.rs index 11caabe6..f128c9b6 100644 --- a/hive-matrix-mcp/src/client.rs +++ b/hive-matrix-mcp/src/client.rs @@ -529,4 +529,41 @@ mod tests { )); assert!(calls(&fake).is_empty()); } + + /// Serve `status` and `body` for every request on loopback; returns the + /// base URL. + async fn stub_homeserver(status: u16, body: &'static str) -> String { + let app = axum::Router::new().fallback(move || async move { + ( + axum::http::StatusCode::from_u16(status).expect("a status"), + [(axum::http::header::CONTENT_TYPE, "application/json")], + body, + ) + }); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("bind"); + let base = format!("http://{}", listener.local_addr().expect("local addr")); + tokio::spawn(async move { axum::serve(listener, app).await }); + base + } + + #[tokio::test] + async fn a_whoami_refusal_does_not_quote_the_response_body() { + let base = + stub_homeserver(502, r#"{"errcode":"M_UNKNOWN","error":"free-form-marker"}"#).await; + let e = whoami(&base, "tok").await.expect_err("refused"); + let message = format!("{e:#}"); + assert!(message.contains("502"), "{message}"); + assert!(!message.contains("free-form-marker"), "{message}"); + } + + #[tokio::test] + async fn whoami_reads_both_ids_and_reports_an_unknown_token_as_none() { + let base = stub_homeserver(200, r#"{"user_id":"@a:x","device_id":"DEV"}"#).await; + let (user, device) = whoami(&base, "tok").await.expect("live").expect("ids"); + assert_eq!((user.as_str(), device.as_str()), ("@a:x", "DEV")); + let base = stub_homeserver(401, r#"{"errcode":"M_UNKNOWN_TOKEN"}"#).await; + assert!(whoami(&base, "tok").await.expect("a verdict").is_none()); + } } diff --git a/swarm-matrix-client/README.md b/swarm-matrix-client/README.md index d7ce5ecd..cbb97454 100644 --- a/swarm-matrix-client/README.md +++ b/swarm-matrix-client/README.md @@ -1,11 +1,11 @@ # swarm-matrix-client -The appservice calls against the swarm's homeserver, shared by the two binaries +The client-server API calls against a Matrix homeserver, shared by the binaries that make them: `swarm-matrix-ctl` (inside the matrix container, for a hive's sender account) and `swarm-controller` (for each agent's account, with the swarm's own appservice token). -Three calls, all client-server API: register an account as the appservice, log +Three appservice calls: register an account as the appservice, log in to an existing one as the appservice, and ask a token who it is. Both mints pin the device id `hyperhive-`, so a second login **replaces** that device's token instead of adding a device. @@ -15,6 +15,12 @@ device's token instead of adding a device. the operator names. Neither call is an appservice call, so neither carries the appservice token. +`hive-c0re` provisions the hive Space and chat room with the room calls +(`joined_rooms`, `room_state`, `put_state`, `create_room`, `invite`, +`resolve_alias`), and `hive-matrix-mcp` recovers a session's user and device id +with `whoami_session`. Each takes the caller's `reqwest::Client`, so the caller +sets its own timeout; `client()` is the 10 second default. + ## 🩸 A secret is a path, never a value Every error here is built from the response's status and its `errcode`, never diff --git a/swarm-matrix-client/src/lib.rs b/swarm-matrix-client/src/lib.rs index 4533f4b9..5a573a0e 100644 --- a/swarm-matrix-client/src/lib.rs +++ b/swarm-matrix-client/src/lib.rs @@ -1,9 +1,13 @@ -//! The appservice calls against the swarm's homeserver: register, log in, ask -//! a token who it is, and the room calls `swarm-controller`'s admin-room -//! commands need (resolve an alias, list members, check an account exists, -//! send a message). Also the password login and logout `swarm-controller` runs -//! for an operator-linked account on any homeserver. Shared by `swarm-matrix-ctl` and `swarm-controller`; -//! see the README for why both mint through the same device id. +//! The calls against a Matrix homeserver's client-server API shared by the +//! swarm and the hive: the appservice calls (register, log in, ask a token who +//! it is), the room calls `swarm-controller`'s admin-room commands and +//! `hive-c0re`'s Space and chat-room provisioning need (resolve an alias, list +//! members and joined rooms, read and set room state, create a room, invite, send +//! a message, check an account exists), and the password login and logout +//! `swarm-controller` runs for an operator-linked account on any homeserver. +//! Shared by `swarm-matrix-ctl`, `swarm-controller`, `hive-c0re` and +//! `hive-matrix-mcp`. See the README for why both appservice mints go through +//! the same device id. //! //! 🩸 **Every error in this module is built from the response's `status` and //! its `errcode`, never its body.** A successful `/register` or `/login` body