From e6c53045ad28b829286081b84adbb83fe97133f3 Mon Sep 17 00:00:00 2001 From: damocles Date: Fri, 29 May 2026 20:19:39 +0200 Subject: [PATCH] matrix: hive-matrix-mcp crate (daemon+stdio bridge) + harness wiring (#548 phase 3) --- CLAUDE.md | 17 + Cargo.lock | 1905 ++++++++++++++++++++++++++++++- Cargo.toml | 4 +- flake.nix | 6 + hive-matrix-mcp/Cargo.toml | 36 + hive-matrix-mcp/src/bin/mcp.rs | 264 +++++ hive-matrix-mcp/src/client.rs | 128 +++ hive-matrix-mcp/src/handlers.rs | 305 +++++ hive-matrix-mcp/src/lib.rs | 14 + hive-matrix-mcp/src/main.rs | 86 ++ hive-matrix-mcp/src/paths.rs | 65 ++ hive-matrix-mcp/src/protocol.rs | 101 ++ hive-matrix-mcp/src/socket.rs | 85 ++ hive-matrix-mcp/src/timeline.rs | 66 ++ hive-matrix-mcp/src/wake.rs | 116 ++ nix/templates/harness-base.nix | 101 ++ 16 files changed, 3287 insertions(+), 12 deletions(-) create mode 100644 hive-matrix-mcp/Cargo.toml create mode 100644 hive-matrix-mcp/src/bin/mcp.rs create mode 100644 hive-matrix-mcp/src/client.rs create mode 100644 hive-matrix-mcp/src/handlers.rs create mode 100644 hive-matrix-mcp/src/lib.rs create mode 100644 hive-matrix-mcp/src/main.rs create mode 100644 hive-matrix-mcp/src/paths.rs create mode 100644 hive-matrix-mcp/src/protocol.rs create mode 100644 hive-matrix-mcp/src/socket.rs create mode 100644 hive-matrix-mcp/src/timeline.rs create mode 100644 hive-matrix-mcp/src/wake.rs diff --git a/CLAUDE.md b/CLAUDE.md index 803ba24a..92216415 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -203,6 +203,23 @@ hive-forge/ Forgejo CLI wrapper (`hive-forge` binary) attach-comment, lint). Replaces the 600-line hive-forge-tools.nix bash script (closes #280). +hive-matrix-mcp/ per-agent matrix-sdk integration (#548 phase 3). + src/main.rs `hive-matrix-daemon` binary entry — long-running + matrix-sdk Client + sync per agent; serves the + MCP bridge over /run/hive-matrix.sock; emits + hyperhive wake on incoming room events. + src/bin/mcp.rs `hive-matrix-mcp` stdio bridge claude spawns per + turn — connects to the daemon socket, forwards + each tool call (send_message, send_dm, + send_reaction, send_reply, mark_read, list_rooms, + list_room_members, read_room). No matrix-sdk dep + at this entrypoint (pure serde + tokio I/O). + src/{client,handlers,socket,timeline,wake,protocol,paths}.rs + shared library modules (matrix-sdk session + restore, request dispatch, unix socket server, + event handler → wake bridge, wire types, env + resolution). + hive-sh4re/ wire types (HostRequest/Response, AgentRequest/Response, ManagerRequest/Response, Message, Approval, HelperEvent) diff --git a/Cargo.lock b/Cargo.lock index 06da1bec..ab9dfd4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,33 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -76,6 +103,105 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "anymap2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" + +[[package]] +name = "aquamarine" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f50776554130342de4836ba542aa85a4ddb361690d7e8df13774d7284c3d5c2" +dependencies = [ + "include_dir", + "itertools 0.10.5", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "archery" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e0a5f99dfebb87bb342d0f53bb92c81842e100bbb915223e38349580e5441d" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +dependencies = [ + "serde", +] + +[[package]] +name = "as_variant" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dbc3a507a82b17ba0d98f6ce8fd6954ea0c8152e98009d36a40d8dcc8ce078a" + +[[package]] +name = "assign" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f093eed78becd229346bf859eec0aa4dd7ddde0757287b2b4107a1f09c80002" + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-compression" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" +dependencies = [ + "compression-codecs", + "compression-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -154,17 +280,57 @@ dependencies = [ "tracing", ] +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bitflags" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitmaps" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6" + +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] [[package]] name = "block-buffer" @@ -175,6 +341,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -187,6 +362,21 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "bytesize" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.2.62" @@ -209,6 +399,30 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.44" @@ -216,11 +430,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + [[package]] name = "clap" version = "4.6.1" @@ -267,6 +494,53 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "compression-codecs" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const_panic" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e262cdaac42494e3ae34c43969f9cdeb7da178bdb4b66fa6a1ea2edb4c8ae652" +dependencies = [ + "typewit", +] + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -282,6 +556,30 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crypto-common" version = "0.1.7" @@ -289,9 +587,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "darling" version = "0.23.0" @@ -332,6 +659,82 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +[[package]] +name = "date_header" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c03c416ed1a30fbb027ef484ba6ab6f80e1eada675e1a2b92fd673c045a1f1d" + +[[package]] +name = "deadpool" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" +dependencies = [ + "deadpool-runtime", + "lazy_static", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" +dependencies = [ + "tokio", +] + +[[package]] +name = "deadpool-sqlite" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8510000b26f632483a35120c2ce280c29e1e14c2dcb27b5055dbdac276f63f58" +dependencies = [ + "deadpool", + "deadpool-sync", + "rusqlite", +] + +[[package]] +name = "deadpool-sync" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524bc3df0d57e98ecd022e21ba31166c2625e7d3e5bcc4510efaeeab4abcab04" +dependencies = [ + "deadpool-runtime", +] + +[[package]] +name = "decancer" +version = "3.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9244323129647178bf41ac861a2cdb9d9c81b9b09d3d0d1de9cd302b33b8a1d" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + [[package]] name = "digest" version = "0.10.7" @@ -340,6 +743,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -359,6 +763,38 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "serde", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.6.4", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + [[package]] name = "equivalent" version = "1.0.2" @@ -375,6 +811,53 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "eyeball" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93bd0ebf93d61d6332d3c09a96e97975968a44e19a64c947bde06e6baff383f" +dependencies = [ + "futures-core", + "readlock", + "readlock-tokio", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "eyeball-im" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43e8e9d31591be508826b875d8fe6056aebcaec3281ac0e45434ff303686c566" +dependencies = [ + "futures-core", + "imbl", + "tokio", + "tracing", +] + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -393,12 +876,34 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" @@ -414,6 +919,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + [[package]] name = "futures" version = "0.3.32" @@ -552,6 +1067,49 @@ dependencies = [ "wasip3", ] +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "growable-bloom-filter" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d174ccb4ba660d431329e7f0797870d0a4281e36353ec4b4a3c5eab6c2cfb6f1" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "xxhash-rust", +] + +[[package]] +name = "h2" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -576,12 +1134,42 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "headers" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +dependencies = [ + "base64", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hive-ag3nt" version = "0.1.0" @@ -638,6 +1226,23 @@ dependencies = [ "serde_json", ] +[[package]] +name = "hive-matrix-mcp" +version = "0.1.0" +dependencies = [ + "anyhow", + "futures-util", + "matrix-sdk", + "reqwest", + "rmcp", + "schemars", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "hive-sh4re" version = "0.1.0" @@ -645,6 +1250,36 @@ dependencies = [ "serde", ] +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever", + "match_token", +] + [[package]] name = "http" version = "1.4.0" @@ -655,6 +1290,15 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-auth" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "150fa4a9462ef926824cf4519c84ed652ca8f4fbae34cb8af045b5cbcaf98822" +dependencies = [ + "memchr", +] + [[package]] name = "http-body" version = "1.0.1" @@ -706,6 +1350,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -895,6 +1540,49 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "imbl" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4308a675e4cfc1920f36a8f4d8fb62d5533b7da106844bd1ec51c6f1fa94a0c" +dependencies = [ + "archery", + "bitmaps", + "imbl-sized-chunks", + "rand_core 0.9.5", + "rand_xoshiro", + "serde", + "version_check", +] + +[[package]] +name = "imbl-sized-chunks" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f4241005618a62f8d57b2febd02510fb96e0137304728543dfc5fd6f052c22d" +dependencies = [ + "bitmaps", +] + +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -907,6 +1595,16 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -919,6 +1617,24 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -937,6 +1653,50 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "js_int" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d937f95470b270ce8b8950207715d71aa8e153c0d44c6684d59397ed4949160a" +dependencies = [ + "serde", +] + +[[package]] +name = "js_option" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68421373957a1593a767013698dbf206e2b221eefe97a44d98d18672ff38423c" +dependencies = [ + "serde", +] + +[[package]] +name = "konst" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97feab15b395d1860944abe6a8dd8ed9f8eadfae01750fada8427abda531d887" +dependencies = [ + "const_panic", + "konst_kernel", + "typewit", +] + +[[package]] +name = "konst_kernel" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4b1eb7788f3824c629b1116a7a9060d6e898c358ebff59070093d51103dcc3c" +dependencies = [ + "typewit", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + [[package]] name = "lazy_static" version = "1.5.0" @@ -978,6 +1738,15 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.29" @@ -990,6 +1759,43 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "matchers" version = "0.2.0" @@ -1005,6 +1811,179 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "matrix-pickle" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c34e6db65145740459f2ca56623b40cd4e6000ffae2a7d91515fa82aa935dbf" +dependencies = [ + "matrix-pickle-derive", + "thiserror 2.0.18", +] + +[[package]] +name = "matrix-pickle-derive" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a962fc9981f823f6555416dcb2ae9ae67ca412d767ee21ecab5150113ee6285b" +dependencies = [ + "proc-macro-crate", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "matrix-sdk" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdfa71339f867dcada2e7f1130f858fd8892088b1a4c123dd50a99ed2399ab22" +dependencies = [ + "anymap2", + "aquamarine", + "as_variant", + "async-channel", + "async-stream", + "async-trait", + "backon", + "bytes", + "bytesize", + "cfg-if", + "event-listener", + "eyeball", + "eyeball-im", + "futures-core", + "futures-util", + "gloo-timers", + "http", + "imbl", + "indexmap", + "itertools 0.14.0", + "js_int", + "language-tags", + "matrix-sdk-base", + "matrix-sdk-common", + "matrix-sdk-sqlite", + "mime", + "mime2ext", + "oauth2", + "once_cell", + "percent-encoding", + "pin-project-lite", + "reqwest", + "ruma", + "serde", + "serde_html_form", + "serde_json", + "sha2", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "url", + "urlencoding", + "vodozemac", + "zeroize", +] + +[[package]] +name = "matrix-sdk-base" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b14659a7e902ea8a821ec217f36b168fb4c79020d91b912175ac188b6c364225" +dependencies = [ + "as_variant", + "async-trait", + "bitflags", + "decancer", + "eyeball", + "eyeball-im", + "futures-util", + "growable-bloom-filter", + "matrix-sdk-common", + "matrix-sdk-store-encryption", + "once_cell", + "regex", + "ruma", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tracing", + "unicode-normalization", +] + +[[package]] +name = "matrix-sdk-common" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb33a986495135e217f28cfe0918bf7d01a9800e42f4ef88afbd48e23b8cc53" +dependencies = [ + "eyeball-im", + "futures-core", + "futures-executor", + "futures-util", + "gloo-timers", + "imbl", + "ruma", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tracing", + "tracing-subscriber", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "matrix-sdk-sqlite" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "662a128004b553196365d88ea46917047d7350a24dfe7ce829e7b78636673604" +dependencies = [ + "as_variant", + "async-trait", + "deadpool-sqlite", + "itertools 0.14.0", + "matrix-sdk-base", + "matrix-sdk-store-encryption", + "num_cpus", + "rmp-serde", + "ruma", + "rusqlite", + "serde", + "serde_json", + "serde_path_to_error", + "thiserror 2.0.18", + "tokio", + "tracing", + "vodozemac", +] + +[[package]] +name = "matrix-sdk-store-encryption" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e0aac550e685306fbbd57faae8f98af9812f19bbf0f83da7559d67cf4789679" +dependencies = [ + "base64", + "blake3", + "chacha20poly1305", + "hmac", + "pbkdf2", + "rand 0.8.6", + "rmp-serde", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.18", + "zeroize", +] + [[package]] name = "memchr" version = "2.8.0" @@ -1017,6 +1996,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime2ext" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf6f36070878c42c5233846cd3de24cf9016828fd47bc22957a687298bb21fc" + [[package]] name = "mime_guess" version = "2.0.5" @@ -1027,6 +2012,16 @@ dependencies = [ "unicase", ] +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.2.0" @@ -1038,6 +2033,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1047,6 +2048,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "num-traits" version = "0.2.19" @@ -1056,6 +2063,36 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "oauth2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" +dependencies = [ + "base64", + "chrono", + "getrandom 0.2.17", + "http", + "rand 0.8.6", + "reqwest", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "thiserror 1.0.69", + "url", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -1068,30 +2105,134 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + [[package]] name = "pastey" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5a797f0e07bdf071d15742978fc3128ec6c22891c31a3a931513263904c982a" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.6", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -1101,6 +2242,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1110,6 +2257,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "prettyplease" version = "0.2.37" @@ -1120,6 +2273,36 @@ dependencies = [ "syn", ] +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -1129,6 +2312,47 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pulldown-cmark" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" +dependencies = [ + "bitflags", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + [[package]] name = "quinn" version = "0.11.9" @@ -1143,7 +2367,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -1158,13 +2382,13 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand", + "rand 0.9.4", "ring", "rustc-hash", "rustls", "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -1205,14 +2429,35 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ - "rand_chacha", - "rand_core", + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", ] [[package]] @@ -1222,7 +2467,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", ] [[package]] @@ -1234,6 +2488,39 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_xoshiro" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "readlock" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6da6f291b23556edd9edaf655a0be2ad8ef8002ff5f1bca62b264f3f58b53f34" + +[[package]] +name = "readlock-tokio" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7e264f9ec4f3d112e8e2f214e8e7cb5cf3b83278f3570b7e00bfe13d3bd8ff" +dependencies = [ + "tokio", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + [[package]] name = "ref-cast" version = "1.0.25" @@ -1254,6 +2541,18 @@ dependencies = [ "syn", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" version = "0.4.14" @@ -1282,6 +2581,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", + "h2", "http", "http-body", "http-body-util", @@ -1302,12 +2602,14 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "webpki-roots", ] @@ -1341,7 +2643,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "thiserror", + "thiserror 2.0.18", "tokio", "tokio-util", "tracing", @@ -1360,6 +2662,201 @@ dependencies = [ "syn", ] +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + +[[package]] +name = "ruma" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7b698b728bc3747f564a9115c83b4f2e229b52377f6a1cca2e6add9cf4a13be" +dependencies = [ + "assign", + "js_int", + "js_option", + "ruma-client-api", + "ruma-common", + "ruma-events", + "ruma-federation-api", + "ruma-html", + "ruma-signatures", + "web-time", +] + +[[package]] +name = "ruma-client-api" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b54e56c591f9ad686defb0bacbebba5c8882eb0c9f8734f6a080345b4e3dd941" +dependencies = [ + "as_variant", + "assign", + "bytes", + "date_header", + "http", + "js_int", + "js_option", + "maplit", + "ruma-common", + "ruma-events", + "serde", + "serde_html_form", + "serde_json", + "thiserror 2.0.18", + "url", + "web-time", +] + +[[package]] +name = "ruma-common" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac7f59b9f7639667d0d6ae3ae242c8912e9ed061cea1fbaf72710a402e83b53e" +dependencies = [ + "as_variant", + "base64", + "bytes", + "form_urlencoded", + "getrandom 0.2.17", + "http", + "indexmap", + "js_int", + "konst", + "percent-encoding", + "rand 0.8.6", + "regex", + "ruma-identifiers-validation", + "ruma-macros", + "serde", + "serde_html_form", + "serde_json", + "thiserror 2.0.18", + "time", + "tracing", + "url", + "uuid", + "web-time", + "wildmatch", + "zeroize", +] + +[[package]] +name = "ruma-events" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34fa815769ed4fe1ef5b50aa0ba6f350317c13b5a9f1e008b014f4a3ddf14204" +dependencies = [ + "as_variant", + "indexmap", + "js_int", + "js_option", + "percent-encoding", + "pulldown-cmark", + "regex", + "ruma-common", + "ruma-identifiers-validation", + "ruma-macros", + "serde", + "serde_json", + "thiserror 2.0.18", + "tracing", + "url", + "web-time", + "wildmatch", + "zeroize", +] + +[[package]] +name = "ruma-federation-api" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecbc887ba1292e48e6363b29e0dec4571b52d2b5102ebf60068105efadaa6e0a" +dependencies = [ + "headers", + "http", + "http-auth", + "httparse", + "js_int", + "memchr", + "mime", + "ruma-common", + "ruma-events", + "serde", + "serde_json", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "ruma-html" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6124d74847ea788601477c89a44485894432a806824cae93885c5825a8ae9dbc" +dependencies = [ + "as_variant", + "html5ever", + "tracing", + "wildmatch", +] + +[[package]] +name = "ruma-identifiers-validation" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14a7b93ac1e571c585f8fa5cef09c07bb8a15529775fd56b9a3eac4f9233dff2" +dependencies = [ + "js_int", + "thiserror 2.0.18", +] + +[[package]] +name = "ruma-macros" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9911c7188517f28505d2d513339511d00e0f50cec5c2dde820cd0ec7e6a833" +dependencies = [ + "cfg-if", + "proc-macro-crate", + "proc-macro2", + "quote", + "ruma-identifiers-validation", + "serde", + "syn", + "toml", +] + +[[package]] +name = "ruma-signatures" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f47cd146d56ae6e7a4a8d912a30dfe57c70e5bf18806fdf617527d4d4f2dd2a4" +dependencies = [ + "base64", + "ed25519-dalek", + "pkcs8", + "rand 0.8.6", + "ruma-common", + "serde_json", + "sha2", + "thiserror 2.0.18", +] + [[package]] name = "rusqlite" version = "0.37.0" @@ -1380,6 +2877,15 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.1.4" @@ -1466,6 +2972,12 @@ dependencies = [ "syn", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "semver" version = "1.0.28" @@ -1482,6 +2994,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -1513,6 +3035,19 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_html_form" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" +dependencies = [ + "form_urlencoded", + "indexmap", + "itoa", + "ryu", + "serde_core", +] + [[package]] name = "serde_json" version = "1.0.149" @@ -1537,6 +3072,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1556,7 +3100,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", "digest", ] @@ -1585,6 +3140,27 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + [[package]] name = "slab" version = "0.4.12" @@ -1607,12 +3183,47 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + [[package]] name = "strsim" version = "0.11.1" @@ -1669,13 +3280,44 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1698,6 +3340,25 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + [[package]] name = "tinystr" version = "0.8.3" @@ -1797,6 +3458,70 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + [[package]] name = "tower" version = "0.5.3" @@ -1819,6 +3544,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ + "async-compression", "bitflags", "bytes", "futures-core", @@ -1931,9 +3657,9 @@ dependencies = [ "http", "httparse", "log", - "rand", + "rand 0.9.4", "sha1", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -1942,6 +3668,21 @@ version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +[[package]] +name = "typewit" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6" +dependencies = [ + "typewit_proc_macros", +] + +[[package]] +name = "typewit_proc_macros" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" + [[package]] name = "unicase" version = "2.9.0" @@ -1954,12 +3695,31 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -1976,8 +3736,21 @@ dependencies = [ "idna", "percent-encoding", "serde", + "serde_derive", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -1990,6 +3763,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -2008,6 +3792,36 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vodozemac" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c022a277687e4e8685d72b95a7ca3ccfec907daa946678e715f8badaa650883d" +dependencies = [ + "aes", + "arrayvec", + "base64", + "base64ct", + "cbc", + "chacha20poly1305", + "curve25519-dalek", + "ed25519-dalek", + "getrandom 0.2.17", + "hkdf", + "hmac", + "matrix-pickle", + "prost", + "rand 0.8.6", + "serde", + "serde_bytes", + "serde_json", + "sha2", + "subtle", + "thiserror 2.0.18", + "x25519-dalek", + "zeroize", +] + [[package]] name = "want" version = "0.3.1" @@ -2118,6 +3932,19 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasmparser" version = "0.244.0" @@ -2159,6 +3986,12 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "wildmatch" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29333c3ea1ba8b17211763463ff24ee84e41c78224c16b001cd907e663a38c68" + [[package]] name = "windows-core" version = "0.62.2" @@ -2374,6 +4207,24 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.51.0" @@ -2474,6 +4325,24 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + [[package]] name = "yoke" version = "0.8.2" @@ -2543,6 +4412,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zerotrie" diff --git a/Cargo.toml b/Cargo.toml index b990949f..327d3cbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = ["hive-ag3nt", "hive-c0re", "hive-forge", "hive-sh4re"] +members = ["hive-ag3nt", "hive-c0re", "hive-forge", "hive-matrix-mcp", "hive-sh4re"] [workspace.package] edition = "2024" @@ -46,3 +46,5 @@ tokio-stream = { version = "0.1", features = ["sync"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +matrix-sdk = { version = "0.14", default-features = false, features = ["rustls-tls", "sqlite", "markdown"] } +futures-util = "0.3" diff --git a/flake.nix b/flake.nix index f6d43a86..1161ae21 100644 --- a/flake.nix +++ b/flake.nix @@ -93,8 +93,14 @@ # rsvg-convert call — that whole codepath moved into the # `hyperhive-assets` derivation in #555, so the rust # derivation no longer needs the dependency. + # `sqlite` required by matrix-sdk's `sqlite` feature + # (`hive-matrix-mcp` workspace member, #548 phase 3) — the + # matrix-sdk-sqlite + rusqlite stack links against system + # libsqlite3 by default. nativeBuildInputs = [ pkgs.git + pkgs.sqlite + pkgs.pkg-config ]; } ); diff --git a/hive-matrix-mcp/Cargo.toml b/hive-matrix-mcp/Cargo.toml new file mode 100644 index 00000000..faa3fe1a --- /dev/null +++ b/hive-matrix-mcp/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "hive-matrix-mcp" +edition.workspace = true +version.workspace = true + +[lints] +workspace = true + +[dependencies] +anyhow.workspace = true +futures-util.workspace = true +matrix-sdk.workspace = true +reqwest.workspace = true +rmcp.workspace = true +schemars.workspace = true +serde.workspace = true +serde_json.workspace = true +tokio.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true + +# `hive-matrix-daemon` — long-running per-agent matrix-sdk Client + +# sync loop. Holds the unix socket the stdio MCP bridge talks to and +# emits hyperhive wake signals on incoming room events. +[[bin]] +name = "hive-matrix-daemon" +path = "src/main.rs" + +# `hive-matrix-mcp` — thin stdio MCP bridge spawned by claude per turn. +# Forwards every tool call to the daemon over /run/hive-matrix.sock, +# returns the daemon's response shape to claude. No matrix-sdk dep at +# this entrypoint — the heavy crate only loads when the daemon binary +# is invoked. +[[bin]] +name = "hive-matrix-mcp" +path = "src/bin/mcp.rs" diff --git a/hive-matrix-mcp/src/bin/mcp.rs b/hive-matrix-mcp/src/bin/mcp.rs new file mode 100644 index 00000000..427f30db --- /dev/null +++ b/hive-matrix-mcp/src/bin/mcp.rs @@ -0,0 +1,264 @@ +//! `hive-matrix-mcp` binary — stdio MCP server claude spawns per turn. +//! Thin protocol bridge: every tool call → connect to the daemon's +//! unix socket → write a JSON request line → read the JSON response → +//! return the payload (or the error message) to claude. +//! +//! No matrix-sdk dep at this entrypoint. The daemon owns the heavy +//! Client + sync; the bridge is pure serde + tokio I/O. Means the MCP +//! binary cold-starts in milliseconds even though the daemon takes +//! seconds to bring up sync. + +use anyhow::{Context, Result}; +use rmcp::{ + ServerHandler, ServiceExt, + handler::server::wrapper::Parameters, + schemars::{self, JsonSchema}, + tool, tool_handler, tool_router, + transport::stdio, +}; +use serde::Deserialize; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::net::UnixStream; + +use hive_matrix_mcp::paths; +use hive_matrix_mcp::protocol::{DaemonRequest, DaemonResponse}; + +/// Send `req` to the daemon and read back the response. Each call is a +/// fresh unix-socket connection — short-lived (the daemon dispatch is +/// a single round-trip) so connection pooling would be over-engineering. +async fn round_trip(req: DaemonRequest) -> Result { + let socket = paths::daemon_socket(); + let stream = UnixStream::connect(&socket) + .await + .with_context(|| format!("connect daemon socket {}", socket.display()))?; + let (reader, mut writer) = stream.into_split(); + let mut line = serde_json::to_string(&req)?; + line.push('\n'); + writer + .write_all(line.as_bytes()) + .await + .context("write request to daemon socket")?; + writer.shutdown().await.ok(); + let mut buf = String::new(); + BufReader::new(reader) + .read_line(&mut buf) + .await + .context("read response from daemon socket")?; + serde_json::from_str(&buf).context("parse daemon response") +} + +/// Turn a `DaemonResponse` into the string claude sees as the tool +/// result. Ok payloads are pretty-printed JSON; errors get a clear +/// "matrix error: …" prefix so claude can pattern-match on it. +fn render(resp: Result) -> String { + match resp { + Ok(DaemonResponse::Ok { payload }) => { + serde_json::to_string_pretty(&payload).unwrap_or_else(|_| payload.to_string()) + } + Ok(DaemonResponse::Error { message }) => format!("matrix error: {message}"), + Err(e) => format!("matrix bridge error: {e:#}"), + } +} + +#[derive(Debug, Deserialize, JsonSchema)] +struct SendMessageArgs { + /// Matrix room id (`!abc:server`) or canonical alias (`#name:server`). + /// Aliases are server-resolved at send time. + room: String, + /// Message body. Markdown is rendered to HTML by the daemon + /// (`text_markdown`); plain text passes through unchanged. + body: String, +} + +#[derive(Debug, Deserialize, JsonSchema)] +struct SendDmArgs { + /// Matrix user id of the recipient (`@user:server`). DM room is + /// created if one doesn't already exist between this agent and + /// the recipient. + user_id: String, + body: String, +} + +#[derive(Debug, Deserialize, JsonSchema)] +struct SendReactionArgs { + room: String, + /// Full matrix event id of the message being reacted to. + event_id: String, + /// Reaction key — usually an emoji (`👍`, `❤️`) but any string + /// works per matrix spec. + key: String, +} + +#[derive(Debug, Deserialize, JsonSchema)] +struct SendReplyArgs { + room: String, + event_id: String, + body: String, +} + +#[derive(Debug, Deserialize, JsonSchema)] +struct MarkReadArgs { + room: String, + event_id: String, +} + +#[derive(Debug, Deserialize, JsonSchema)] +struct ListRoomsArgs {} + +#[derive(Debug, Deserialize, JsonSchema)] +struct ListRoomMembersArgs { + room: String, +} + +#[derive(Debug, Deserialize, JsonSchema)] +struct ReadRoomArgs { + room: String, + /// Maximum events to return (default 50, max 200). Newest first. + #[serde(default)] + limit: Option, +} + +struct MatrixBridge { + #[allow(dead_code)] + tool_router: rmcp::handler::server::router::tool::ToolRouter, +} + +impl MatrixBridge { + fn new() -> Self { + Self { + tool_router: Self::tool_router(), + } + } +} + +#[tool_router] +impl MatrixBridge { + #[tool( + description = "Post a plain-text or markdown message to a matrix room. \ + `room` is either a room id (!abc:server) or alias (#name:server). \ + Returns the new event id." + )] + async fn send_message(&self, Parameters(args): Parameters) -> String { + render(round_trip(DaemonRequest::SendMessage { + room: args.room, + body: args.body, + }).await) + } + + #[tool( + description = "Open (or reuse) a direct message room with `user_id` \ + (@user:server) and post `body` to it." + )] + async fn send_dm(&self, Parameters(args): Parameters) -> String { + render(round_trip(DaemonRequest::SendDm { + user_id: args.user_id, + body: args.body, + }).await) + } + + #[tool( + description = "React to a specific matrix event with an emoji or short \ + string `key`. Matrix-spec annotation; renders as a reaction in \ + standard clients." + )] + async fn send_reaction(&self, Parameters(args): Parameters) -> String { + render(round_trip(DaemonRequest::SendReaction { + room: args.room, + event_id: args.event_id, + key: args.key, + }).await) + } + + #[tool( + description = "Reply to a specific matrix event in a room, threaded \ + via m.in_reply_to. Returns the reply's event id." + )] + async fn send_reply(&self, Parameters(args): Parameters) -> String { + render(round_trip(DaemonRequest::SendReply { + room: args.room, + event_id: args.event_id, + body: args.body, + }).await) + } + + #[tool( + description = "Mark a specific event as read for this agent. Updates \ + the room's unread indicator + sends a read receipt other \ + participants can see." + )] + async fn mark_read(&self, Parameters(args): Parameters) -> String { + render(round_trip(DaemonRequest::MarkRead { + room: args.room, + event_id: args.event_id, + }).await) + } + + #[tool( + description = "List rooms this agent has joined. Each row has the room \ + id, canonical alias (when set), display name, and joined-member count." + )] + async fn list_rooms(&self, Parameters(_): Parameters) -> String { + render(round_trip(DaemonRequest::ListRooms).await) + } + + #[tool( + description = "List the members of a matrix room (joined-state only). \ + Each row carries the user id and resolved display name." + )] + async fn list_room_members(&self, Parameters(args): Parameters) -> String { + render(round_trip(DaemonRequest::ListRoomMembers { room: args.room }).await) + } + + #[tool( + description = "Read the most recent N events from a matrix room \ + (default 50, max 200). Returns each event's id, sender, timestamp, \ + type, and best-effort plain-text body." + )] + async fn read_room(&self, Parameters(args): Parameters) -> String { + render(round_trip(DaemonRequest::ReadRoom { + room: args.room, + limit: args.limit, + }).await) + } +} + +#[tool_handler( + instructions = "Matrix client for an agent on a hyperhive swarm. Use \ + `send_message` to post in a joined room, `send_dm` to message a \ + specific user, `send_reaction` to react with an emoji, `send_reply` \ + to thread a reply, `mark_read` to acknowledge an event. Discover \ + rooms with `list_rooms`, members with `list_room_members`, recent \ + timeline with `read_room`. Room references accept ids (!abc:server) \ + or aliases (#name:server); user references use @user:server." +)] +impl ServerHandler for MatrixBridge {} + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")), + ) + .with_writer(std::io::stderr) + .init(); + + // Standalone-degraded boot: missing daemon socket → exit 0 + // cleanly so claude doesn't see an MCP startup error when matrix + // hasn't been provisioned for this agent yet. The daemon will + // appear once hive-c0re writes the token + systemd starts the + // daemon unit. + let socket = paths::daemon_socket(); + if !tokio::fs::try_exists(&socket).await.unwrap_or(false) { + tracing::warn!( + path = %socket.display(), + "matrix daemon socket absent; exiting cleanly so MCP startup doesn't fail" + ); + return Ok(()); + } + + let bridge = MatrixBridge::new(); + let service = bridge.serve(stdio()).await.context("serve MCP over stdio")?; + service.waiting().await.context("MCP service exited unexpectedly")?; + Ok(()) +} diff --git a/hive-matrix-mcp/src/client.rs b/hive-matrix-mcp/src/client.rs new file mode 100644 index 00000000..50bf92a3 --- /dev/null +++ b/hive-matrix-mcp/src/client.rs @@ -0,0 +1,128 @@ +//! 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. +//! +//! No OAuth dance / cross-signing setup (in contrast to damocles-daemon's +//! ccc.de connection): for the in-hive tuwunel the `registration_token` +//! UIAA flow already minted the token + user/device, hive-c0re just +//! handed us the bearer in a file. matrix-sdk's `restore_session` with +//! a constructed `MatrixSession` skips the login flow entirely. + +use std::path::Path; + +use anyhow::{Context, Result, anyhow}; +use matrix_sdk::{ + Client, SessionMeta, SessionTokens, + authentication::matrix::MatrixSession, + ruma::{OwnedDeviceId, OwnedUserId}, +}; +use serde::Deserialize; +use tokio::fs; + +/// 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, +} + +/// 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. +/// +/// Steps: +/// 1. Read the bearer token from `token_file` (trim trailing whitespace). +/// 2. 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 +/// 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` +/// using a synthetic `MatrixSession`. +pub async fn build_and_restore( + homeserver: &str, + token_file: &Path, + state_dir: &Path, +) -> Result { + 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) = whoami(homeserver, &token).await?; + + fs::create_dir_all(state_dir) + .await + .with_context(|| format!("mkdir matrix state dir {}", state_dir.display()))?; + let client = Client::builder() + .homeserver_url(homeserver) + .sqlite_store(state_dir, None) + .build() + .await + .with_context(|| format!("build matrix client for {homeserver}"))?; + let session = MatrixSession { + meta: SessionMeta { user_id, device_id }, + tokens: SessionTokens { + access_token: token, + refresh_token: None, + }, + }; + client + .restore_session(session) + .await + .context("restore matrix session")?; + tracing::info!( + user = %client.user_id().map(ToString::to_string).unwrap_or_default(), + device = %client.device_id().map(ToString::to_string).unwrap_or_default(), + "matrix session restored" + ); + 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 + .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)) +} diff --git a/hive-matrix-mcp/src/handlers.rs b/hive-matrix-mcp/src/handlers.rs new file mode 100644 index 00000000..387b4726 --- /dev/null +++ b/hive-matrix-mcp/src/handlers.rs @@ -0,0 +1,305 @@ +//! Per-tool dispatch — each daemon request from the MCP bridge resolves +//! to one of these handlers. Returns a `DaemonResponse` shaped for the +//! wire protocol (the MCP bridge unwraps `Ok { payload }` and returns +//! the payload to claude; `Error { message }` becomes the tool-call +//! error message claude sees). +//! +//! Tool surface mirrors damocles-daemon's v0 set per mara on #548: +//! `send_message`, `send_dm`, `send_reaction`, `send_reply`, `mark_read`, +//! `list_rooms`, `list_room_members`, `read_room`. Plus a `ping` for the +//! MCP bridge's liveness probe. + +use matrix_sdk::{ + Client, + room::reply::{EnforceThread, Reply}, + ruma::{ + OwnedEventId, OwnedRoomId, OwnedUserId, RoomOrAliasId, + api::client::receipt::create_receipt::v3::ReceiptType, + events::{ + receipt::ReceiptThread, + reaction::ReactionEventContent, + relation::Annotation, + room::message::{MessageType, RoomMessageEventContent}, + }, + }, +}; +use serde::Serialize; + +use crate::protocol::DaemonResponse; + +/// JSON-shape for `list_rooms`: one row per joined room. +#[derive(Debug, Serialize)] +pub struct RoomInfo { + pub room_id: String, + pub canonical_alias: Option, + pub name: String, + pub member_count: u64, +} + +/// JSON-shape for `list_room_members`: one row per joined member. +#[derive(Debug, Serialize)] +pub struct MemberInfo { + pub user_id: String, + pub display_name: Option, +} + +/// JSON-shape for `read_room`: one timeline event, flattened for +/// claude consumption. +#[derive(Debug, Serialize)] +pub struct TimelineEvent { + pub event_id: String, + pub sender: String, + pub origin_server_ts: i64, + pub event_type: String, + /// Best-effort plain-text body for `m.room.message` events + /// (text / notice / emote); empty for events without a textual + /// body (state changes, reactions, etc.). + pub body: String, +} + +/// Resolve a room reference (id `!abc:server` OR alias `#name:server`) +/// to a joined `Room`. Returns an `Error` response if the room isn't +/// joined / the reference is malformed. +async fn resolve_room( + client: &Client, + reference: &str, +) -> Result { + let parsed: &RoomOrAliasId = reference.try_into().map_err(|e| { + DaemonResponse::error(format!("invalid room reference {reference}: {e}")) + })?; + let room_id: OwnedRoomId = if parsed.is_room_id() { + OwnedRoomId::try_from(reference) + .map_err(|e| DaemonResponse::error(format!("invalid room_id: {e}")))? + } else { + client + .resolve_room_alias(parsed.as_str().try_into().map_err(|e| { + DaemonResponse::error(format!("invalid alias: {e}")) + })?) + .await + .map(|r| r.room_id) + .map_err(|e| DaemonResponse::error(format!("resolve_room_alias {reference}: {e}")))? + }; + client + .get_room(&room_id) + .ok_or_else(|| DaemonResponse::error(format!("room {room_id} not joined"))) +} + +/// Best-effort plain-text body for a (non-sync) timeline event. +/// Returns "" for non-text events (state changes, reactions, +/// redactions) — claude can still see the `event_type` field to +/// disambiguate. `AnyTimelineEvent` (not `AnySync...`) because +/// `read_room` pulls events via the `/messages` endpoint which +/// returns the full-form variant. +fn extract_body(event: &matrix_sdk::ruma::events::AnyTimelineEvent) -> String { + use matrix_sdk::ruma::events::{AnyMessageLikeEvent, AnyTimelineEvent}; + match event { + AnyTimelineEvent::MessageLike(AnyMessageLikeEvent::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, + Err(e) => return e, + }; + let content = RoomMessageEventContent::text_markdown(body); + match room.send(content).await { + Ok(resp) => DaemonResponse::ok(&serde_json::json!({ + "event_id": resp.event_id.to_string(), + "room_id": room.room_id().to_string(), + })), + Err(e) => DaemonResponse::error(format!("send to {}: {e}", room.room_id())), + } +} + +pub async fn send_dm(client: &Client, user_id: &str, body: &str) -> DaemonResponse { + let uid: OwnedUserId = match user_id.parse() { + Ok(u) => u, + Err(e) => return DaemonResponse::error(format!("invalid user_id {user_id}: {e}")), + }; + // Find existing DM or create one. + let room = client + .joined_rooms() + .into_iter() + .find(|r| { + // is_direct() is async; check direct_targets() instead which + // reads from cached state. + r.direct_targets().iter().any(|t| t.as_str() == uid.as_str()) + }); + let room = match room { + Some(r) => r, + None => match client.create_dm(&uid).await { + Ok(r) => r, + Err(e) => return DaemonResponse::error(format!("create_dm {uid}: {e}")), + }, + }; + let content = RoomMessageEventContent::text_markdown(body); + match room.send(content).await { + Ok(resp) => DaemonResponse::ok(&serde_json::json!({ + "event_id": resp.event_id.to_string(), + "room_id": room.room_id().to_string(), + "user_id": uid.to_string(), + })), + Err(e) => DaemonResponse::error(format!("send DM to {uid}: {e}")), + } +} + +pub async fn send_reaction( + client: &Client, + room_ref: &str, + event_id: &str, + key: &str, +) -> DaemonResponse { + let room = match resolve_room(client, room_ref).await { + Ok(r) => r, + Err(e) => return e, + }; + let eid: OwnedEventId = match event_id.parse() { + Ok(e) => e, + Err(e) => return DaemonResponse::error(format!("invalid event_id {event_id}: {e}")), + }; + let content = ReactionEventContent::new(Annotation::new(eid.clone(), key.to_owned())); + match room.send(content).await { + Ok(resp) => DaemonResponse::ok(&serde_json::json!({ + "event_id": resp.event_id.to_string(), + "target": eid.to_string(), + })), + Err(e) => DaemonResponse::error(format!("send reaction to {eid}: {e}")), + } +} + +pub async fn send_reply( + client: &Client, + room_ref: &str, + event_id: &str, + body: &str, +) -> DaemonResponse { + let room = match resolve_room(client, room_ref).await { + Ok(r) => r, + Err(e) => return e, + }; + let eid: OwnedEventId = match event_id.parse() { + Ok(e) => e, + Err(e) => return DaemonResponse::error(format!("invalid event_id {event_id}: {e}")), + }; + let content = RoomMessageEventContent::text_markdown(body).into(); + let reply = Reply { + event_id: eid.clone(), + enforce_thread: EnforceThread::MaybeThreaded, + }; + let reply_content = match room.make_reply_event(content, reply).await { + Ok(c) => c, + Err(e) => return DaemonResponse::error(format!("make_reply_event: {e}")), + }; + match room.send(reply_content).await { + Ok(resp) => DaemonResponse::ok(&serde_json::json!({ + "event_id": resp.event_id.to_string(), + "target": eid.to_string(), + })), + Err(e) => DaemonResponse::error(format!("send reply: {e}")), + } +} + +pub async fn mark_read(client: &Client, room_ref: &str, event_id: &str) -> DaemonResponse { + let room = match resolve_room(client, room_ref).await { + Ok(r) => r, + Err(e) => return e, + }; + let eid: OwnedEventId = match event_id.parse() { + Ok(e) => e, + Err(e) => return DaemonResponse::error(format!("invalid event_id {event_id}: {e}")), + }; + match room + .send_single_receipt(ReceiptType::Read, ReceiptThread::Unthreaded, eid.clone()) + .await + { + Ok(()) => DaemonResponse::ok(&serde_json::json!({ + "marked_read": eid.to_string(), + })), + Err(e) => DaemonResponse::error(format!("send_single_receipt: {e}")), + } +} + +pub async fn list_rooms(client: &Client) -> DaemonResponse { + let mut rooms = Vec::new(); + for room in client.joined_rooms() { + let name = match room.display_name().await { + Ok(n) => n.to_string(), + Err(_) => room.room_id().to_string(), + }; + let canonical_alias = room.canonical_alias().map(|a| a.to_string()); + rooms.push(RoomInfo { + room_id: room.room_id().to_string(), + canonical_alias, + name, + member_count: room.joined_members_count(), + }); + } + DaemonResponse::ok(&rooms) +} + +pub async fn list_room_members(client: &Client, room_ref: &str) -> DaemonResponse { + let room = match resolve_room(client, room_ref).await { + Ok(r) => r, + Err(e) => return e, + }; + let members = match room.members(matrix_sdk::RoomMemberships::JOIN).await { + Ok(m) => m, + Err(e) => return DaemonResponse::error(format!("members: {e}")), + }; + let list: Vec = members + .iter() + .map(|m| MemberInfo { + user_id: m.user_id().to_string(), + display_name: m.display_name().map(ToOwned::to_owned), + }) + .collect(); + DaemonResponse::ok(&list) +} + +pub async fn read_room(client: &Client, room_ref: &str, limit: Option) -> DaemonResponse { + use matrix_sdk::ruma::api::client::message::get_message_events; + use matrix_sdk::ruma::api::Direction; + + 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 events: Vec = resp + .chunk + .iter() + .filter_map(|raw| { + let parsed = 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); + Some(TimelineEvent { + event_id, + sender, + origin_server_ts, + event_type, + body, + }) + }) + .collect(); + DaemonResponse::ok(&events) +} diff --git a/hive-matrix-mcp/src/lib.rs b/hive-matrix-mcp/src/lib.rs new file mode 100644 index 00000000..8db22f9a --- /dev/null +++ b/hive-matrix-mcp/src/lib.rs @@ -0,0 +1,14 @@ +//! `hive-matrix-mcp` library: shared types + helpers used by both the +//! `hive-matrix-daemon` binary (long-running matrix-sdk Client) and the +//! `hive-matrix-mcp` binary (stdio MCP bridge claude spawns per turn). +//! +//! The wire protocol between the two binaries lives in [`protocol`]. +//! Path helpers (token file, daemon socket) live in [`paths`]. +//! +//! Phase 3 of #548. Architecture rationale + tool surface mirror the +//! existing `damocles-daemon` (see issue thread for details). + +pub mod client; +pub mod paths; +pub mod protocol; +pub mod wake; diff --git a/hive-matrix-mcp/src/main.rs b/hive-matrix-mcp/src/main.rs new file mode 100644 index 00000000..34a6b0bb --- /dev/null +++ b/hive-matrix-mcp/src/main.rs @@ -0,0 +1,86 @@ +//! `hive-matrix-daemon` binary — long-running matrix-sdk Client + sync +//! loop per agent. Bridges incoming room events to hyperhive wake +//! signals and serves the unix socket the stdio MCP bridge talks to. +//! +//! Lifecycle: +//! 1. Read access token from `paths::token_file()` (fail clean if absent). +//! 2. Whoami probe → recover `user_id` + `device_id` → restore +//! matrix-sdk session (no login flow). +//! 3. Install the message-event handler that fires hyperhive wakes. +//! 4. Spawn the unix socket listener for the MCP bridge. +//! 5. Run sync forever. +//! +//! Standalone-degraded boot: missing token file → exit 0 cleanly so +//! systemd's `ConditionPathExists=` doesn't have to be perfectly +//! synced with hive-c0re's token-provisioning timing. + +use anyhow::{Context, Result}; +use matrix_sdk::config::SyncSettings; + +mod client; +mod handlers; +mod paths; +mod protocol; +mod socket; +mod timeline; +mod wake; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), + ) + .with_writer(std::io::stderr) + .init(); + + let homeserver = paths::homeserver_url(); + let token_file = paths::token_file(); + let state_dir = paths::matrix_state_dir(); + let mcp_socket = paths::daemon_socket(); + let hyperhive_socket = paths::hyperhive_socket(); + + if !tokio::fs::try_exists(&token_file).await.unwrap_or(false) { + tracing::warn!( + path = %token_file.display(), + "matrix token file absent; exiting cleanly (hive-c0re will provision \ + it on first agent registration, then systemd restarts us)" + ); + return Ok(()); + } + + tracing::info!( + homeserver, + token_file = %token_file.display(), + state_dir = %state_dir.display(), + "hive-matrix-daemon starting" + ); + + let matrix_client = client::build_and_restore(&homeserver, &token_file, &state_dir) + .await + .context("build matrix client")?; + + timeline::install_message_handler(&matrix_client, hyperhive_socket); + + // Spawn the unix socket server before sync starts so the MCP + // bridge can connect as soon as the first claude turn fires. The + // socket dispatches against the same `Client` we sync on, so any + // tool call benefits from the sync state-cache. + let socket_client = matrix_client.clone(); + let socket_listener = mcp_socket.clone(); + tokio::spawn(async move { + if let Err(e) = socket::serve(&socket_listener, socket_client).await { + tracing::error!(error = %e, "mcp socket server exited"); + } + }); + + // Sync forever; matrix-sdk handles reconnection internally. + let sync_settings = SyncSettings::default(); + matrix_client + .sync(sync_settings) + .await + .context("matrix-sdk sync loop exited")?; + + Ok(()) +} diff --git a/hive-matrix-mcp/src/paths.rs b/hive-matrix-mcp/src/paths.rs new file mode 100644 index 00000000..7d497108 --- /dev/null +++ b/hive-matrix-mcp/src/paths.rs @@ -0,0 +1,65 @@ +//! Per-agent filesystem paths used by both `hive-matrix-daemon` and the +//! stdio MCP bridge. +//! +//! All paths are overridable via env vars for dev / test scenarios and +//! to let the harness point the daemon at non-default locations when +//! the operator overrides `hyperhive.matrix.*` options. + +use std::path::PathBuf; + +/// Default homeserver URL when `HIVE_MATRIX_URL` isn't set. Tuwunel +/// (the local hive-matrix container) listens on `localhost:8008` by +/// default; shared host netns means every agent container resolves +/// `localhost` to the same machine. +pub const DEFAULT_HOMESERVER: &str = "http://localhost:8008"; + +/// Default unix socket path the daemon listens on inside the agent +/// container. The stdio MCP bridge `connect()`s here on every tool call. +/// `/run/hive-matrix.sock` is a tmpfs path that disappears on container +/// restart — fine, because the daemon recreates the socket on its own +/// boot. +pub const DEFAULT_DAEMON_SOCKET: &str = "/run/hive-matrix.sock"; + +/// Resolve the matrix access-token file path. Override via +/// `HIVE_MATRIX_TOKEN_FILE`; default is `/matrix-token`, +/// the path `hive-c0re::matrix::ensure_user_for` writes to on agent +/// account provisioning. +#[must_use] +pub fn token_file() -> PathBuf { + if let Some(p) = std::env::var_os("HIVE_MATRIX_TOKEN_FILE") { + return PathBuf::from(p); + } + let state_dir = std::env::var("HYPERHIVE_STATE_DIR").unwrap_or_default(); + PathBuf::from(format!("{state_dir}/matrix-token")) +} + +/// Resolve the homeserver URL. Override via `HIVE_MATRIX_URL`; default +/// is the in-container `localhost:8008` tuwunel. +#[must_use] +pub fn homeserver_url() -> String { + std::env::var("HIVE_MATRIX_URL").unwrap_or_else(|_| DEFAULT_HOMESERVER.to_owned()) +} + +/// Resolve the daemon's unix socket path. Override via +/// `HIVE_MATRIX_SOCKET`; default is `/run/hive-matrix.sock`. +#[must_use] +pub fn daemon_socket() -> PathBuf { + std::env::var_os("HIVE_MATRIX_SOCKET").map_or_else(|| PathBuf::from(DEFAULT_DAEMON_SOCKET), PathBuf::from) +} + +/// Persistent sqlite store directory for matrix-sdk's state (event +/// cache, devices, etc.). Lives under the per-agent state dir so it +/// survives container restart but gets wiped on `destroy --purge`. +#[must_use] +pub fn matrix_state_dir() -> PathBuf { + let state_dir = std::env::var("HYPERHIVE_STATE_DIR").unwrap_or_default(); + PathBuf::from(format!("{state_dir}/matrix-sdk-state")) +} + +/// Hyperhive control socket — the daemon writes wake signals here so +/// the harness drives a new claude turn on incoming matrix events. +/// Mirrors the path `forge_notify` writes to. +#[must_use] +pub fn hyperhive_socket() -> PathBuf { + std::env::var_os("HIVE_CONTROL_SOCKET").map_or_else(|| PathBuf::from("/run/hive/mcp.sock"), PathBuf::from) +} diff --git a/hive-matrix-mcp/src/protocol.rs b/hive-matrix-mcp/src/protocol.rs new file mode 100644 index 00000000..efc5affd --- /dev/null +++ b/hive-matrix-mcp/src/protocol.rs @@ -0,0 +1,101 @@ +//! Wire types for the daemon ↔ stdio-MCP-bridge unix socket protocol. +//! +//! Same shape as `damocles-daemon`'s `DaemonRequest`/`DaemonResponse` +//! (which this is forked from in spirit). Each request is a single +//! JSON line; each response is a single JSON line back. The stdio MCP +//! bridge holds a fresh connection per tool call — claude's tool +//! lifecycle is shorter than a persistent matrix-sdk Client wants to +//! live, so the daemon stays alive and the MCP reconnects per call. + +use serde::{Deserialize, Serialize}; + +/// Request from the stdio MCP bridge to the daemon. The MCP bridge +/// owns the on-wire shape claude sees; this enum is the internal +/// shape the daemon dispatches over. +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "method")] +pub enum DaemonRequest { + /// Post a plain-text or markdown message to a room. `room` accepts + /// either a matrix room id (`!abc:server`) or a canonical alias + /// (`#name:server`); the daemon resolves aliases server-side. + #[serde(rename = "send_message")] + SendMessage { room: String, body: String }, + + /// Open (or reuse) a DM with `user_id` and post `body`. Creates + /// the DM room if one doesn't already exist between this agent + /// and the user. + #[serde(rename = "send_dm")] + SendDm { user_id: String, body: String }, + + /// React to a specific event with an emoji `key`. Matrix-spec + /// `m.reaction` annotation. + #[serde(rename = "send_reaction")] + SendReaction { + room: String, + event_id: String, + key: String, + }, + + /// Reply to `event_id` in `room` with `body` as a threaded reply. + /// Sets the `m.in_reply_to` relation so matrix clients render the + /// thread. + #[serde(rename = "send_reply")] + SendReply { + room: String, + event_id: String, + body: String, + }, + + /// Mark `event_id` (in `room`) as read for this agent. Sends a + /// read receipt; bumps the room's "unread" indicator down on + /// matrix clients (and for other agents). + #[serde(rename = "mark_read")] + MarkRead { room: String, event_id: String }, + + /// List rooms the agent has joined. Returns each room's id + + /// canonical alias (when present) + name + member count. + #[serde(rename = "list_rooms")] + ListRooms, + + /// List the members of a room. Each entry carries the matrix + /// user id + the resolved display name (when set). + #[serde(rename = "list_room_members")] + ListRoomMembers { room: String }, + + /// Read the last `limit` events from a room's timeline. Caller + /// gets each event's id, sender, `server_ts`, type, and body (best- + /// effort plain-text extraction from `m.text` / `m.notice` etc.). + #[serde(rename = "read_room")] + ReadRoom { room: String, limit: Option }, + + /// Liveness probe used by the stdio MCP bridge on connect — fast + /// "are you up?" round-trip that doesn't touch matrix-sdk. + #[serde(rename = "ping")] + Ping, +} + +/// Response shape: `ok` carries the payload (any JSON; the MCP bridge +/// passes it back to claude as the tool result), `error` carries a +/// human-readable error string. +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum DaemonResponse { + Ok { payload: serde_json::Value }, + Error { message: String }, +} + +impl DaemonResponse { + /// Convenience: build an Ok response from any serializable value. + pub fn ok(payload: &T) -> Self { + Self::Ok { + payload: serde_json::to_value(payload).unwrap_or(serde_json::Value::Null), + } + } + + /// Convenience: build an Error response from any `Display` value. + pub fn error(msg: impl std::fmt::Display) -> Self { + Self::Error { + message: msg.to_string(), + } + } +} diff --git a/hive-matrix-mcp/src/socket.rs b/hive-matrix-mcp/src/socket.rs new file mode 100644 index 00000000..5e0d6b9b --- /dev/null +++ b/hive-matrix-mcp/src/socket.rs @@ -0,0 +1,85 @@ +//! Unix socket server: the daemon listens here, the stdio MCP bridge +//! `connect()`s on every tool call. One JSON request line in, one +//! JSON response line out. Connections are short-lived (per tool call) +//! so the loop is just accept → dispatch → reply → close. + +use std::path::Path; +use std::sync::Arc; + +use anyhow::{Context, Result}; +use matrix_sdk::Client; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::net::{UnixListener, UnixStream}; + +use crate::handlers; +use crate::protocol::{DaemonRequest, DaemonResponse}; + +/// Start listening on `socket_path` and serve forever. Removes any +/// stale socket file first (daemon restart after a non-clean shutdown +/// would otherwise hit EADDRINUSE). +pub async fn serve(socket_path: &Path, client: Client) -> Result<()> { + let _ = tokio::fs::remove_file(socket_path).await; + if let Some(parent) = socket_path.parent() { + tokio::fs::create_dir_all(parent) + .await + .with_context(|| format!("mkdir {}", parent.display()))?; + } + let listener = UnixListener::bind(socket_path) + .with_context(|| format!("bind unix socket {}", socket_path.display()))?; + tracing::info!(path = %socket_path.display(), "mcp socket listener up"); + let client = Arc::new(client); + loop { + let (stream, _) = listener + .accept() + .await + .context("accept connection on mcp socket")?; + let client = client.clone(); + tokio::spawn(async move { + if let Err(e) = handle_connection(stream, &client).await { + tracing::warn!(error = %e, "mcp socket connection error"); + } + }); + } +} + +async fn handle_connection(stream: UnixStream, client: &Client) -> Result<()> { + let (reader, mut writer) = stream.into_split(); + let mut lines = BufReader::new(reader).lines(); + while let Some(line) = lines.next_line().await? { + let response = match serde_json::from_str::(&line) { + Ok(req) => dispatch(req, client).await, + Err(e) => DaemonResponse::error(format!("parse request: {e}")), + }; + let mut json = serde_json::to_string(&response)?; + json.push('\n'); + writer.write_all(json.as_bytes()).await?; + writer.flush().await?; + } + Ok(()) +} + +async fn dispatch(req: DaemonRequest, client: &Client) -> DaemonResponse { + match req { + DaemonRequest::Ping => DaemonResponse::ok(&serde_json::json!({"ok": true})), + DaemonRequest::SendMessage { room, body } => { + handlers::send_message(client, &room, &body).await + } + DaemonRequest::SendDm { user_id, body } => handlers::send_dm(client, &user_id, &body).await, + DaemonRequest::SendReaction { + room, + event_id, + key, + } => handlers::send_reaction(client, &room, &event_id, &key).await, + DaemonRequest::SendReply { + room, + event_id, + body, + } => handlers::send_reply(client, &room, &event_id, &body).await, + DaemonRequest::MarkRead { room, event_id } => { + handlers::mark_read(client, &room, &event_id).await + } + DaemonRequest::ListRooms => handlers::list_rooms(client).await, + DaemonRequest::ListRoomMembers { room } => handlers::list_room_members(client, &room).await, + DaemonRequest::ReadRoom { room, limit } => handlers::read_room(client, &room, limit).await, + } +} diff --git a/hive-matrix-mcp/src/timeline.rs b/hive-matrix-mcp/src/timeline.rs new file mode 100644 index 00000000..2b695497 --- /dev/null +++ b/hive-matrix-mcp/src/timeline.rs @@ -0,0 +1,66 @@ +//! Matrix event handlers: incoming room messages fire a hyperhive +//! wake signal so the agent's harness drives a new claude turn. +//! +//! Per mara on #548: wake body is a SHORT TEASER, not the full message +//! (msg stays unread server-side; agent fetches via `read_room`). The +//! `wake::format_wake_body` truncates to ~100 chars. +//! +//! Self-events (events sent by this agent) are filtered out so an +//! agent posting a message doesn't wake itself. + +use std::path::PathBuf; +use std::sync::Arc; + +use matrix_sdk::{ + Client, Room, RoomState, + ruma::events::room::message::{MessageType, OriginalSyncRoomMessageEvent}, +}; + +use crate::wake; + +/// Install the room-message handler on `client`. Fires on every +/// `m.room.message` event in a joined room; non-self text messages +/// trigger a wake signal to the hyperhive harness via the unix socket +/// at `hyperhive_socket`. +pub fn install_message_handler(client: &Client, hyperhive_socket: PathBuf) { + let socket = Arc::new(hyperhive_socket); + let own_user = client.user_id().map(std::borrow::ToOwned::to_owned); + client.add_event_handler({ + let socket = socket.clone(); + move |event: OriginalSyncRoomMessageEvent, room: Room, _client: Client| { + let socket = socket.clone(); + let own_user = own_user.clone(); + async move { + if room.state() != RoomState::Joined { + return; + } + // Self-event filter so the agent's own outgoing messages + // don't wake it. matches forge_notify's self-skip pattern. + if own_user.as_ref().is_some_and(|u| u == &event.sender) { + return; + } + let text = match &event.content.msgtype { + MessageType::Text(t) => t.body.clone(), + MessageType::Notice(n) => n.body.clone(), + MessageType::Emote(e) => format!("* {}", e.body), + _ => { + // Non-text content (image / file / location / etc.) — + // still wake, but with a placeholder body so the + // agent knows something landed and can read_room. + format!("[{}]", event.content.msgtype()) + } + }; + let room_label = room + .canonical_alias() + .map_or_else(|| room.room_id().to_string(), |a| a.to_string()); + let body = wake::format_wake_body(event.sender.as_str(), &room_label, &text); + if let Err(e) = wake::send_wake(&socket, &body).await { + tracing::warn!(error = %e, "failed to deliver matrix wake to hyperhive"); + } else { + tracing::debug!(room = %room.room_id(), "matrix wake delivered"); + } + } + } + }); + tracing::info!("matrix message handler installed"); +} diff --git a/hive-matrix-mcp/src/wake.rs b/hive-matrix-mcp/src/wake.rs new file mode 100644 index 00000000..39099157 --- /dev/null +++ b/hive-matrix-mcp/src/wake.rs @@ -0,0 +1,116 @@ +//! Wake-signal writer: notifies the hyperhive harness when an incoming +//! matrix event arrives so claude drives a new turn. +//! +//! Same wire shape as `hive-ag3nt::forge_notify`'s wake: a single JSON +//! line written to the hyperhive control socket (`/run/hive/mcp.sock` +//! by default) carrying an `AgentRequest::Wake { from, body }`. +//! The agent harness's `agent_server` parses it and treats it as a +//! `Wake` from the matrix subsystem. +//! +//! Per mara's call on #548 phase 3: the body is a SHORT TEASER, not +//! the full message — the agent then reads the unmarked event via +//! the `read_room` MCP tool. Truncation to ~100 chars keeps the wake +//! prompt focused (`forge_notify` embeds longer excerpts because the +//! agent doesn't have a follow-up read-the-original tool for forge). + +use std::path::Path; + +use anyhow::{Context, Result}; +use tokio::io::AsyncWriteExt; +use tokio::net::UnixStream; + +/// Max characters of `body` to embed in the wake payload. Shorter than +/// `forge_notify`'s 500-byte excerpt because the agent has a follow-up +/// `read_room` tool to fetch the full event. +pub const WAKE_BODY_TRUNCATE: usize = 100; + +/// Send an `AgentRequest::Wake { from: "matrix", body }` to the hyperhive +/// control socket at `socket`. Best-effort: returns Err on any plumbing +/// failure; callers log + ignore so a wake delivery hiccup doesn't tear +/// down the matrix sync loop. +pub async fn send_wake(socket: &Path, body: impl AsRef) -> Result<()> { + let payload = serde_json::json!({ + "kind": "wake", + "from": "matrix", + "body": body.as_ref(), + }); + let line = format!("{}\n", serde_json::to_string(&payload)?); + let mut stream = UnixStream::connect(socket) + .await + .with_context(|| format!("connect hyperhive socket {}", socket.display()))?; + stream + .write_all(line.as_bytes()) + .await + .with_context(|| format!("write wake to {}", socket.display()))?; + stream + .shutdown() + .await + .with_context(|| format!("shutdown write to {}", socket.display()))?; + Ok(()) +} + +/// Format a wake-message body from a matrix event's sender + room +/// canonical alias + body text. Truncates at [`WAKE_BODY_TRUNCATE`] +/// chars with an ellipsis. Shape: `[matrix] in : ` +/// matches the `forge_notify` `[issue …]` framing convention. +#[must_use] +pub fn format_wake_body(sender: &str, room: &str, text: &str) -> String { + let truncated = truncate_chars(text, WAKE_BODY_TRUNCATE); + format!("[matrix] {sender} in {room}: {truncated}") +} + +/// Truncate `s` to `max` Unicode chars, appending `…` when cut. +/// Char-based not byte-based so multi-byte content (most chat) doesn't +/// get cut mid-codepoint. +fn truncate_chars(s: &str, max: usize) -> String { + let mut end = s.len(); + for (count, (i, _)) in s.char_indices().enumerate() { + if count == max { + end = i; + break; + } + } + if end == s.len() { + s.to_owned() + } else { + format!("{}…", &s[..end]) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn format_wake_body_short_passes_through() { + let body = format_wake_body("@iris:matrix.darkest.space", "#general", "hi all"); + assert_eq!(body, "[matrix] @iris:matrix.darkest.space in #general: hi all"); + } + + #[test] + fn format_wake_body_long_truncates_with_ellipsis() { + let long = "x".repeat(200); + let body = format_wake_body("@iris:m", "#x", &long); + assert!(body.contains("xxxxxxx")); + assert!(body.ends_with("…")); + // Header + truncated body should be well under the absolute + // wake-message ceiling (forge_notify uses ~600 bytes; we're + // way under that). + assert!(body.len() < 200); + } + + #[test] + fn truncate_chars_handles_multibyte() { + // `ü` is 2 bytes / 1 char. truncating to 3 chars on "üüüüüü" + // should yield "üüü…" not "üü\xc3…" (mid-codepoint). + let s = "üüüüüü"; + let t = truncate_chars(s, 3); + assert_eq!(t, "üüü…"); + } + + #[test] + fn truncate_chars_no_op_below_limit() { + let s = "hi"; + assert_eq!(truncate_chars(s, 100), "hi"); + } +} diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index 3a8eb1f5..227e8c01 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -149,6 +149,52 @@ ''; }; + options.hyperhive.matrix.enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Enable per-agent matrix integration via `hive-matrix-mcp` + (#548 phase 3). When true (the default), the harness: + + - runs `hive-matrix-daemon` as a systemd unit that holds a + matrix-sdk Client + sync against the homeserver at + `HIVE_MATRIX_URL` (default `http://localhost:8008` — the + in-host tuwunel from `nix/modules/hive-matrix.nix`). The + daemon auto-skips when `/matrix-token` is missing, + and a `systemd.paths` watcher restarts it the moment + hive-c0re provisions the token (mirrors `matrix-avatar-sync` + shape from #571). + - exposes the matrix tool surface (send_message, send_dm, + send_reaction, send_reply, mark_read, list_rooms, + list_room_members, read_room) to claude via an auto-injected + `extraMcpServers.matrix` entry. Claude spawns the stdio + `hive-matrix-mcp` bridge per turn, which forwards each tool + call to the daemon over `/run/hive-matrix.sock`. + - wakes the agent on incoming room events via a short teaser + Wake signal (`[matrix] in : …`) + to the hyperhive control socket; the full event stays + unread server-side until `read_room` consumes it. + + Set to `false` for agents that should NOT have matrix tools at + all (e.g. agents on a host without `hyperhive.matrix.enable` on + the meta side). When token file is absent the daemon and MCP + both no-op cleanly anyway, so `false` is rarely necessary. + ''; + }; + + options.hyperhive.matrix.url = lib.mkOption { + type = lib.types.str; + default = "http://localhost:8008"; + example = "https://matrix.darkest.space"; + description = '' + Matrix homeserver URL the agent's `hive-matrix-daemon` connects + to. Default points at the in-host tuwunel (shared netns). + Override per-agent when an agent should talk to an external + homeserver instead (e.g. a federation-only setup or a remote + hive's tuwunel reached via a vpn). + ''; + }; + options.hyperhive.frontend.dist = lib.mkOption { type = lib.types.package; default = pkgs.hyperhive-frontend; @@ -483,6 +529,19 @@ } ]; + # Auto-inject the matrix MCP entry when matrix is enabled (#548 + # phase 3). Operator can override or disable by setting their own + # `extraMcpServers.matrix` (nix submodule merge takes the operator's + # value) or by flipping `hyperhive.matrix.enable = false`. + hyperhive.extraMcpServers = lib.mkIf config.hyperhive.matrix.enable { + matrix = lib.mkDefault { + command = "${pkgs.hyperhive}/bin/hive-matrix-mcp"; + args = [ ]; + env = { }; + allowedTools = [ "*" ]; + }; + }; + environment.etc."hyperhive/extra-mcp.json".text = builtins.toJSON config.hyperhive.extraMcpServers; # Operator-set per-agent icon (hyperhive.icon). When configured, the @@ -745,6 +804,48 @@ ''; }; + # Long-running matrix-sdk Client + sync per agent (#548 phase 3). + # Holds the unix socket the stdio `hive-matrix-mcp` bridge talks + # to, and emits hyperhive wake signals on incoming room events + # via `/run/hive/mcp.sock`. Conditional on `hyperhive.matrix.enable` + # AND token-file presence (the daemon binary itself exits 0 on + # missing token, but the path watcher below restarts it the + # moment the token lands — same first-boot-ordering pattern as + # matrix-avatar-sync.path / #571). + systemd.services.hive-matrix-daemon = lib.mkIf config.hyperhive.matrix.enable { + description = "long-running matrix-sdk Client + MCP daemon socket"; + wantedBy = [ "multi-user.target" ]; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + environment = { + HIVE_MATRIX_URL = config.hyperhive.matrix.url; + RUST_LOG = "info"; + }; + serviceConfig = { + ExecStart = "${pkgs.hyperhive}/bin/hive-matrix-daemon"; + Restart = "on-failure"; + RestartSec = 5; + # /run/hive-matrix.sock + the matrix-sdk-state sqlite dir + # don't need a StateDirectory= — the socket is on tmpfs (gone + # on restart, which is correct) and the sqlite dir lives in + # the bind-mounted agent state, mode-managed by the harness. + }; + }; + + # Path-trigger sibling so hive-matrix-daemon fires the moment + # `/matrix-token` appears (#548 phase 3, mirrors the + # matrix-avatar-sync.path pattern from #571). On clean boot + # hive-c0re provisions the token AFTER agent containers come up; + # without the trigger the daemon would exit 0 quietly and the + # MCP would have no backend until next restart. With the watcher + # the daemon comes alive in the same boot cycle as provisioning. + systemd.paths.hive-matrix-daemon = lib.mkIf config.hyperhive.matrix.enable { + description = "trigger hive-matrix-daemon when matrix-token appears"; + wantedBy = [ "multi-user.target" ]; + pathConfig.PathExists = "/state/matrix-token"; + pathConfig.PathExistsGlob = "/agents/*/state/matrix-token"; + }; + # Path-trigger sibling so matrix-avatar-sync fires the moment # `/matrix-token` appears (#571 closes argus's first-boot # ordering nag on #567). On a clean boot hive-c0re's matrix