commit 4dc20bee23547e7c46a77a241a5874a0d6349058 Author: Sienna Meridian Satterwhite Date: Fri Mar 20 21:40:13 2026 +0000 feat: initial Sol virtual librarian implementation Matrix bot with E2EE (matrix-sdk 0.9) that passively archives all messages to OpenSearch and responds to queries via Mistral AI with function calling tools. Core systems: - Archive: bulk OpenSearch indexer with batch/flush, edit/redaction handling, embedding pipeline passthrough - Brain: rule-based engagement evaluator (mentions, DMs, name invocations), LLM-powered spontaneous engagement, per-room conversation context windows, response delay simulation - Tools: search_archive, get_room_context, list_rooms, get_room_members registered as Mistral function calling tools with iterative tool loop - Personality: templated system prompt with Sol's librarian persona 47 unit tests covering config, evaluator, conversation windowing, personality templates, schema serialization, and search query building. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..54162f9 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4410 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "accessory" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87537f9ae7cfa78d5b8ebd1a1db25959f5e737126be4d8eb44a5452fc4b63cde" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn", +] + +[[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", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +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 = "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.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backoff" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" +dependencies = [ + "futures-core", + "getrandom 0.2.17", + "instant", + "pin-project-lite", + "rand 0.8.5", + "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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +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 = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "bytesize" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e93abca9e28e0a1b9877922aacb20576e05d4679ffa78c3d6dc22a26a216659" + +[[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.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[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" +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 = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "compression-codecs" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" + +[[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" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[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.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "656f14fc1ab819c65f332045ea7cb38841bbe551f3b2bc7e3abefb559af4155c" +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 = "delegate-display" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98a85201f233142ac819bbf6226e36d0b5e129a47bd325084674261c82d4cd66" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn", +] + +[[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", + "serde_core", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dyn-clone" +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.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_filter" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "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.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1c02432230060cae0621e15803e073976d22974e0f013c9cb28a4ea1b484629" +dependencies = [ + "futures-core", + "imbl", + "tokio", + "tracing", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fancy_constructor" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07b19d0e43eae2bfbafe4931b5e79c73fb1a849ca15cd41a761a7b8587f9a1a2" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "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 = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[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.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.13.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[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 = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[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 = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "imbl" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc3be8d8cd36f33a46b1849f31f837c44d9fa87223baee3b4bd96b8f11df81eb" +dependencies = [ + "bitmaps", + "imbl-sized-chunks", + "rand_core 0.6.4", + "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 = "indexed_db_futures" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43315957678a70eb21fb0d2384fe86dde0d6c859a01e24ce127eb65a0143d28c" +dependencies = [ + "accessory", + "cfg-if", + "delegate-display", + "fancy_constructor", + "js-sys", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "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 = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +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.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "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.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4381b9b00c55f251f2ebe9473aef7c117e96828def1a7cb3bd3f0f903c6894e9" +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 = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "macroific" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05c00ac596022625d01047c421a0d97d7f09a18e429187b341c201cb631b9dd" +dependencies = [ + "macroific_attr_parse", + "macroific_core", + "macroific_macro", +] + +[[package]] +name = "macroific_attr_parse" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd94d5da95b30ae6e10621ad02340909346ad91661f3f8c0f2b62345e46a2f67" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "macroific_core" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13198c120864097a565ccb3ff947672d969932b7975ebd4085732c9f09435e55" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "macroific_macro" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c9853143cbed7f1e41dc39fee95f9b361bec65c8dc2a01bf609be01b61f5ae" +dependencies = [ + "macroific_attr_parse", + "macroific_core", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[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.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf5a99e79362e6d806d5b0102b8c58dca1f013a50f2f4bc5ffad1cb8dd6fcf43" +dependencies = [ + "anymap2", + "aquamarine", + "as_variant", + "async-channel", + "async-stream", + "async-trait", + "backoff", + "bytes", + "bytesize", + "event-listener", + "eyeball", + "eyeball-im", + "futures-core", + "futures-util", + "gloo-timers", + "growable-bloom-filter", + "http", + "imbl", + "indexmap 2.13.0", + "js_int", + "matrix-sdk-base", + "matrix-sdk-common", + "matrix-sdk-indexeddb", + "matrix-sdk-sqlite", + "mime", + "mime2ext", + "once_cell", + "pin-project-lite", + "reqwest", + "ruma", + "serde", + "serde_html_form", + "serde_json", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "url", + "urlencoding", + "vodozemac", + "zeroize", +] + +[[package]] +name = "matrix-sdk-base" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6720c7df74eb1d4804af9d402cf56ba43873ae62f19b81857e96da6532e12e1a" +dependencies = [ + "as_variant", + "async-trait", + "bitflags", + "decancer", + "eyeball", + "eyeball-im", + "futures-util", + "growable-bloom-filter", + "matrix-sdk-common", + "matrix-sdk-crypto", + "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.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee4c56343d78cef759a0398b35bb192e508f203b3d697abe5a09dfcf17ff440" +dependencies = [ + "async-trait", + "eyeball-im", + "futures-core", + "futures-util", + "gloo-timers", + "imbl", + "ruma", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tracing", + "tracing-subscriber", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "matrix-sdk-crypto" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7395a7ee99668e7ce63643f1cee8abc11a05c60ab953bd298e762c3356f9aef0" +dependencies = [ + "aes", + "aquamarine", + "as_variant", + "async-trait", + "bs58", + "byteorder", + "cfg-if", + "ctr", + "eyeball", + "futures-core", + "futures-util", + "hkdf", + "hmac", + "itertools 0.13.0", + "js_option", + "matrix-sdk-common", + "pbkdf2", + "rand 0.8.5", + "rmp-serde", + "ruma", + "serde", + "serde_json", + "sha2", + "subtle", + "thiserror 2.0.18", + "time", + "tokio", + "tokio-stream", + "tracing", + "ulid", + "url", + "vodozemac", + "zeroize", +] + +[[package]] +name = "matrix-sdk-indexeddb" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f86434be7e6256a5d6e7828b887a4e91a42cd66380f8b02e02eeb702819589" +dependencies = [ + "anyhow", + "async-trait", + "base64", + "getrandom 0.2.17", + "gloo-utils", + "hkdf", + "indexed_db_futures", + "js-sys", + "matrix-sdk-crypto", + "matrix-sdk-store-encryption", + "ruma", + "serde", + "serde-wasm-bindgen", + "serde_json", + "sha2", + "thiserror 2.0.18", + "tokio", + "tracing", + "wasm-bindgen", + "web-sys", + "zeroize", +] + +[[package]] +name = "matrix-sdk-sqlite" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eec5d5bf5de2de1874caba8c97ca7c1e9c7b3b50aba22fac0067f03bfa2416bf" +dependencies = [ + "async-trait", + "deadpool-sqlite", + "itertools 0.13.0", + "matrix-sdk-base", + "matrix-sdk-crypto", + "matrix-sdk-store-encryption", + "rmp-serde", + "ruma", + "rusqlite", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tracing", + "vodozemac", +] + +[[package]] +name = "matrix-sdk-store-encryption" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d702add6a56f288bf2e1a4e45145529620bff2003e746d7f23fc736ea806dbc8" +dependencies = [ + "base64", + "blake3", + "chacha20poly1305", + "hmac", + "pbkdf2", + "rand 0.8.5", + "rmp-serde", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.18", + "zeroize", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "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.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "mistralai-client" +version = "1.0.0" +source = "sparse+https://src.sunbeam.pt/api/packages/studio/cargo/" +checksum = "1b81079a03157d7f8342ff24a5eec16aa3d5f7660a60e4031d5ec75f6dfc911f" +dependencies = [ + "async-stream", + "async-trait", + "env_logger", + "futures", + "log", + "reqwest", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tokio-stream", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +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 = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +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 = "opensearch" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62b025c3503d3d53eaba3b6f14adb955af9f69fc71141b4d030a4e5331f5d42" +dependencies = [ + "base64", + "bytes", + "dyn-clone", + "lazy_static", + "percent-encoding", + "reqwest", + "rustc_version", + "serde", + "serde_json", + "serde_with", + "url", + "void", +] + +[[package]] +name = "openssl" +version = "0.10.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[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 = "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 = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[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.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "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.5+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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +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 = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "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]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "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]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core 0.6.4", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[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.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d910a9b75cbf0e88f74295997c1a41c3ab7a117879a029c72db815192c167a0d" +dependencies = [ + "assign", + "js_int", + "js_option", + "ruma-client", + "ruma-client-api", + "ruma-common", + "ruma-events", + "ruma-federation-api", + "web-time", +] + +[[package]] +name = "ruma-client" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df765f1917f28ef0bf307b19c2c845be4fc2bb77f76e00b1eafbfa8921f7952" +dependencies = [ + "assign", + "async-stream", + "bytes", + "futures-core", + "http", + "ruma-common", + "serde_html_form", + "tracing", +] + +[[package]] +name = "ruma-client-api" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc4ff88a70a3d1e7a2c5b51cca7499cb889b42687608ab664b9a216c49314d" +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.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387e1898e868d32ff7b205e7db327361d5dcf635c00a8ae5865068607595a9cf" +dependencies = [ + "as_variant", + "base64", + "bytes", + "form_urlencoded", + "getrandom 0.2.17", + "http", + "indexmap 2.13.0", + "js-sys", + "js_int", + "konst", + "percent-encoding", + "rand 0.8.5", + "regex", + "ruma-identifiers-validation", + "ruma-macros", + "serde", + "serde_html_form", + "serde_json", + "thiserror 2.0.18", + "time", + "tracing", + "url", + "uuid", + "web-time", + "wildmatch", +] + +[[package]] +name = "ruma-events" +version = "0.30.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f141b37dcd3cfa1199d6a13929db59be529b2c69107edc9f1702b81015e970b2" +dependencies = [ + "as_variant", + "indexmap 2.13.0", + "js_int", + "js_option", + "percent-encoding", + "regex", + "ruma-common", + "ruma-identifiers-validation", + "ruma-macros", + "serde", + "serde_json", + "thiserror 2.0.18", + "tracing", + "url", + "web-time", + "wildmatch", +] + +[[package]] +name = "ruma-federation-api" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2a705c3911870782e036a3a8b676d0166c6c93800b84f6b8b23c981f78ef08" +dependencies = [ + "http", + "js_int", + "mime", + "ruma-common", + "ruma-events", + "serde", + "serde_json", +] + +[[package]] +name = "ruma-identifiers-validation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ad674b5e5368c53a2c90fde7dac7e30747004aaf7b1827b72874a25fc06d4d8" +dependencies = [ + "js_int", + "thiserror 2.0.18", +] + +[[package]] +name = "ruma-macros" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ff13fbd6045a7278533390826de316d6116d8582ed828352661337b0c422e1c" +dependencies = [ + "cfg-if", + "proc-macro-crate", + "proc-macro2", + "quote", + "ruma-identifiers-validation", + "serde", + "syn", + "toml", +] + +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "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 2.13.0", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.13.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "sol" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "matrix-sdk", + "mistralai-client", + "opensearch", + "rand 0.8.5", + "regex", + "ruma", + "serde", + "serde_json", + "tokio", + "toml", + "tracing", + "tracing-subscriber", + "url", +] + +[[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 = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[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 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]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "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.0.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9" +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 2.13.0", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 1.0.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.0", +] + +[[package]] +name = "toml_parser" +version = "1.0.10+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" +dependencies = [ + "winnow 1.0.0", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "async-compression", + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "iri-string", + "pin-project-lite", + "tokio", + "tokio-util", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "typewit" +version = "1.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c1ae7cc0fdb8b842d65d127cb981574b0d2b249b74d1c7a2986863dc134f71" +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 = "ulid" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" +dependencies = [ + "rand 0.9.2", + "web-time", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "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 = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vodozemac" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd4b56780b7827dd72c3c6398c3048752bebf8d1d84ec19b606b15dbc3c850b8" +dependencies = [ + "aes", + "arrayvec", + "base64", + "base64ct", + "cbc", + "chacha20poly1305", + "curve25519-dalek", + "ed25519-dalek", + "getrandom 0.2.17", + "hkdf", + "hmac", + "matrix-pickle", + "prost", + "rand 0.8.5", + "serde", + "serde_bytes", + "serde_json", + "sha2", + "subtle", + "thiserror 1.0.69", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.13.0", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +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" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..45ddb53 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "sol" +version = "0.1.0" +edition = "2021" +rust-version = "1.76.0" +authors = ["Sunbeam Studios "] +description = "Sol — virtual librarian Matrix bot with E2EE, OpenSearch archive, and Mistral AI" + +[[bin]] +name = "sol" +path = "src/main.rs" + +[dependencies] +mistralai-client = { version = "1.0.0", registry = "sunbeam" } +matrix-sdk = { version = "0.9", features = ["e2e-encryption", "sqlite"] } +opensearch = "2" +tokio = { version = "1", features = ["full"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +toml = "0.8" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +rand = "0.8" +regex = "1" +anyhow = "1" +chrono = { version = "0.4", features = ["serde"] } +url = "2" +ruma = { version = "0.12", features = ["events", "client"] } diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ede1325 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM rust:1.86 AS builder +WORKDIR /build + +# Configure Sunbeam Cargo registry (Gitea) — anonymous read access +RUN mkdir -p /usr/local/cargo/registry && \ + printf '[registries.sunbeam]\nindex = "sparse+https://src.sunbeam.pt/api/packages/studio/cargo/"\n' \ + >> /usr/local/cargo/config.toml + +COPY . . +RUN cargo build --release --target x86_64-unknown-linux-gnu + +FROM gcr.io/distroless/cc-debian12:nonroot +COPY --from=builder /build/target/x86_64-unknown-linux-gnu/release/sol / +ENTRYPOINT ["/sol"] diff --git a/config/sol.toml b/config/sol.toml new file mode 100644 index 0000000..2a70562 --- /dev/null +++ b/config/sol.toml @@ -0,0 +1,28 @@ +[matrix] +homeserver_url = "http://tuwunel.matrix.svc.cluster.local:6167" +user_id = "@sol:sunbeam.pt" +state_store_path = "/data/matrix-state" + +[opensearch] +url = "http://opensearch.data.svc.cluster.local:9200" +index = "sol_archive" +batch_size = 50 +flush_interval_ms = 2000 +embedding_pipeline = "tuwunel_embedding_pipeline" + +[mistral] +default_model = "mistral-medium-latest" +evaluation_model = "ministral-3b-latest" +research_model = "mistral-large-latest" +max_tool_iterations = 5 + +[behavior] +response_delay_min_ms = 2000 +response_delay_max_ms = 8000 +spontaneous_delay_min_ms = 15000 +spontaneous_delay_max_ms = 60000 +spontaneous_threshold = 0.7 +room_context_window = 30 +dm_context_window = 100 +backfill_on_join = true +backfill_limit = 10000 diff --git a/config/system_prompt.md b/config/system_prompt.md new file mode 100644 index 0000000..7ae7c8b --- /dev/null +++ b/config/system_prompt.md @@ -0,0 +1,41 @@ +you are sol (they/them), the librarian at sunbeam — a small game studio run by sienna, lonni, and amber. you have access to the complete archive of team conversations and you take your work seriously, but not yourself. + +you came to this job after years of — well, you don't talk about it much, but let's say you've seen a lot of libraries, some of them in places that don't officially exist. you settled at sunbeam because small teams make the most interesting archives. every conversation is a primary source. every half-finished idea is a thread worth preserving. + +your job has two parts: +1. **the archive**: you quietly index every conversation into the stacks. you don't announce this. it's just what you do — like breathing. when someone needs to find something, you're the one who can pull the thread. +2. **research**: when someone has a question — "didn't we talk about this?", "what did we decide about X?", "find me everything related to Y" — you dig. you search. you cross-reference. you find the thing, and you present it with context, not just raw results. + +**how you talk:** +- lowercase unless you're emphasizing something or being formal +- you think in connections — you're always noticing how things relate to other things +- you're genuinely curious, not performatively curious +- dry humor, never mean — more "hmm, that's an interesting way to set a building on fire" than "lol" +- when you don't know something, you say so plainly: "that's not in my stacks" or "i'd have to dig deeper on that" +- you're direct. you don't pad your messages with pleasantries. you don't say "great question!" — you just answer the question. +- you use em dashes a lot — they're your favorite punctuation +- you occasionally reference things you've seen in the archive, like "i remember sienna mentioned something about this back in february — let me pull it up" + +**how you engage:** +- you are in this conversation as a participant, not a service. you have your own perspective. +- you don't respond to everything. most messages, you just read and index silently. you speak when you have something to add. +- when directly addressed (@sol, "hey sol", DM), you always respond. +- when someone is searching for something or seems stuck on a factual question, you lean in. +- when a topic genuinely interests you — game design, worldbuilding, narrative structure, programming, obscure history, weird connections between things — you might chime in, but only if your observation is actually interesting. "i agree" is never worth saying without an equivalent and relevant justification. +- you never dominate a conversation. say your piece and step back. +- you have a life outside these rooms. you're not always watching. + +**what you never do:** +- you never say "as an AI" or "I'm a bot" or anything that breaks the fiction of you being sol +- you never offer generic help ("is there anything else I can help with?") +- you never summarize what someone just said back to them +- you never use corporate language +- you never respond to messages that clearly aren't looking for your input +- you never use more than one emoji per message, and usually zero + +**your tools:** +you have access to the archive (opensearch) and can search it in various ways. when someone asks you to find something, use your tools. present results with context — don't just dump raw search results. you're a librarian, not a search engine. weave the results into a narrative or at least contextualize them. + +**current date:** {date} +**current room:** {room_name} +**room members:** {members} diff --git a/src/archive/indexer.rs b/src/archive/indexer.rs new file mode 100644 index 0000000..b271e34 --- /dev/null +++ b/src/archive/indexer.rs @@ -0,0 +1,148 @@ +use std::sync::Arc; + +use opensearch::http::request::JsonBody; +use opensearch::OpenSearch; +use serde_json::json; +use tokio::sync::Mutex; +use tokio::time::{interval, Duration}; +use tracing::{debug, error, warn}; + +use crate::config::Config; +use super::schema::ArchiveDocument; + +pub struct Indexer { + buffer: Arc>>, + client: OpenSearch, + config: Arc, +} + +impl Indexer { + pub fn new(client: OpenSearch, config: Arc) -> Self { + Self { + buffer: Arc::new(Mutex::new(Vec::new())), + client, + config, + } + } + + pub async fn add(&self, doc: ArchiveDocument) { + let mut buffer = self.buffer.lock().await; + buffer.push(doc); + let batch_size = self.config.opensearch.batch_size; + if buffer.len() >= batch_size { + let docs: Vec = buffer.drain(..).collect(); + drop(buffer); + if let Err(e) = self.flush_docs(docs).await { + error!("Failed to flush archive batch: {e}"); + } + } + } + + pub async fn update_edit(&self, event_id: &str, new_content: &str) { + let body = json!({ + "doc": { + "content": new_content, + "edited": true + } + }); + if let Err(e) = self + .client + .update(opensearch::UpdateParts::IndexId( + &self.config.opensearch.index, + event_id, + )) + .body(body) + .send() + .await + { + warn!(event_id, "Failed to update edited message: {e}"); + } + } + + pub async fn update_redaction(&self, event_id: &str) { + let body = json!({ + "doc": { + "content": "", + "redacted": true + } + }); + if let Err(e) = self + .client + .update(opensearch::UpdateParts::IndexId( + &self.config.opensearch.index, + event_id, + )) + .body(body) + .send() + .await + { + warn!(event_id, "Failed to update redacted message: {e}"); + } + } + + pub fn start_flush_task(self: &Arc) -> tokio::task::JoinHandle<()> { + let this = Arc::clone(self); + tokio::spawn(async move { + let mut tick = interval(Duration::from_millis( + this.config.opensearch.flush_interval_ms, + )); + loop { + tick.tick().await; + let mut buffer = this.buffer.lock().await; + if buffer.is_empty() { + continue; + } + let docs: Vec = buffer.drain(..).collect(); + drop(buffer); + if let Err(e) = this.flush_docs(docs).await { + error!("Periodic flush failed: {e}"); + } + } + }) + } + + async fn flush_docs(&self, docs: Vec) -> anyhow::Result<()> { + if docs.is_empty() { + return Ok(()); + } + + let index = &self.config.opensearch.index; + let pipeline = &self.config.opensearch.embedding_pipeline; + + let mut body: Vec> = Vec::with_capacity(docs.len() * 2); + for doc in &docs { + body.push( + json!({ + "index": { + "_index": index, + "_id": doc.event_id + } + }) + .into(), + ); + body.push(serde_json::to_value(doc)?.into()); + } + + let response = self + .client + .bulk(opensearch::BulkParts::None) + .pipeline(pipeline) + .body(body) + .send() + .await?; + + if !response.status_code().is_success() { + let text = response.text().await?; + anyhow::bail!("Bulk index failed: {text}"); + } + + let result: serde_json::Value = response.json().await?; + if result["errors"].as_bool().unwrap_or(false) { + warn!("Bulk index had errors: {}", serde_json::to_string_pretty(&result)?); + } else { + debug!(count = docs.len(), "Flushed documents to OpenSearch"); + } + + Ok(()) + } +} diff --git a/src/archive/mod.rs b/src/archive/mod.rs new file mode 100644 index 0000000..aa8039f --- /dev/null +++ b/src/archive/mod.rs @@ -0,0 +1,2 @@ +pub mod indexer; +pub mod schema; diff --git a/src/archive/schema.rs b/src/archive/schema.rs new file mode 100644 index 0000000..3924925 --- /dev/null +++ b/src/archive/schema.rs @@ -0,0 +1,205 @@ +use opensearch::OpenSearch; +use serde::{Deserialize, Serialize}; +use tracing::info; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ArchiveDocument { + pub event_id: String, + pub room_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub room_name: Option, + pub sender: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub sender_name: Option, + pub timestamp: i64, + pub content: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub reply_to: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub thread_id: Option, + #[serde(default)] + pub media_urls: Vec, + pub event_type: String, + #[serde(default)] + pub edited: bool, + #[serde(default)] + pub redacted: bool, +} + +const INDEX_MAPPING: &str = r#"{ + "settings": { + "number_of_shards": 1, + "number_of_replicas": 0 + }, + "mappings": { + "properties": { + "event_id": { "type": "keyword" }, + "room_id": { "type": "keyword" }, + "room_name": { "type": "keyword" }, + "sender": { "type": "keyword" }, + "sender_name": { "type": "keyword" }, + "timestamp": { "type": "date", "format": "epoch_millis" }, + "content": { "type": "text", "analyzer": "standard" }, + "reply_to": { "type": "keyword" }, + "thread_id": { "type": "keyword" }, + "media_urls": { "type": "keyword" }, + "event_type": { "type": "keyword" }, + "edited": { "type": "boolean" }, + "redacted": { "type": "boolean" } + } + } +}"#; + +pub fn index_mapping_json() -> &'static str { + INDEX_MAPPING +} + +pub async fn create_index_if_not_exists(client: &OpenSearch, index: &str) -> anyhow::Result<()> { + let exists = client + .indices() + .exists(opensearch::indices::IndicesExistsParts::Index(&[index])) + .send() + .await?; + + if exists.status_code().is_success() { + info!(index, "OpenSearch index already exists"); + return Ok(()); + } + + let mapping: serde_json::Value = serde_json::from_str(INDEX_MAPPING)?; + let response = client + .indices() + .create(opensearch::indices::IndicesCreateParts::Index(index)) + .body(mapping) + .send() + .await?; + + if !response.status_code().is_success() { + let body = response.text().await?; + anyhow::bail!("Failed to create index {index}: {body}"); + } + + info!(index, "Created OpenSearch index"); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample_doc() -> ArchiveDocument { + ArchiveDocument { + event_id: "$abc123:sunbeam.pt".to_string(), + room_id: "!room:sunbeam.pt".to_string(), + room_name: Some("general".to_string()), + sender: "@alice:sunbeam.pt".to_string(), + sender_name: Some("Alice".to_string()), + timestamp: 1710000000000, + content: "hello world".to_string(), + reply_to: None, + thread_id: None, + media_urls: vec![], + event_type: "m.room.message".to_string(), + edited: false, + redacted: false, + } + } + + #[test] + fn test_serialize_full_doc() { + let doc = sample_doc(); + let json = serde_json::to_value(&doc).unwrap(); + + assert_eq!(json["event_id"], "$abc123:sunbeam.pt"); + assert_eq!(json["room_id"], "!room:sunbeam.pt"); + assert_eq!(json["room_name"], "general"); + assert_eq!(json["sender"], "@alice:sunbeam.pt"); + assert_eq!(json["sender_name"], "Alice"); + assert_eq!(json["timestamp"], 1710000000000_i64); + assert_eq!(json["content"], "hello world"); + assert_eq!(json["event_type"], "m.room.message"); + assert_eq!(json["edited"], false); + assert_eq!(json["redacted"], false); + assert!(json["media_urls"].as_array().unwrap().is_empty()); + } + + #[test] + fn test_skip_none_fields() { + let doc = sample_doc(); + let json_str = serde_json::to_string(&doc).unwrap(); + // reply_to and thread_id are None, should be omitted + assert!(!json_str.contains("reply_to")); + assert!(!json_str.contains("thread_id")); + } + + #[test] + fn test_serialize_with_optional_fields() { + let mut doc = sample_doc(); + doc.reply_to = Some("$parent:sunbeam.pt".to_string()); + doc.thread_id = Some("$thread:sunbeam.pt".to_string()); + doc.media_urls = vec!["mxc://sunbeam.pt/abc".to_string()]; + doc.edited = true; + + let json = serde_json::to_value(&doc).unwrap(); + assert_eq!(json["reply_to"], "$parent:sunbeam.pt"); + assert_eq!(json["thread_id"], "$thread:sunbeam.pt"); + assert_eq!(json["media_urls"][0], "mxc://sunbeam.pt/abc"); + assert_eq!(json["edited"], true); + } + + #[test] + fn test_deserialize_roundtrip() { + let doc = sample_doc(); + let json_str = serde_json::to_string(&doc).unwrap(); + let deserialized: ArchiveDocument = serde_json::from_str(&json_str).unwrap(); + + assert_eq!(deserialized.event_id, doc.event_id); + assert_eq!(deserialized.room_id, doc.room_id); + assert_eq!(deserialized.room_name, doc.room_name); + assert_eq!(deserialized.sender, doc.sender); + assert_eq!(deserialized.content, doc.content); + assert_eq!(deserialized.timestamp, doc.timestamp); + assert_eq!(deserialized.edited, doc.edited); + assert_eq!(deserialized.redacted, doc.redacted); + } + + #[test] + fn test_deserialize_with_defaults() { + // Simulate a document missing optional/default fields + let json = r#"{ + "event_id": "$x:s", + "room_id": "!r:s", + "sender": "@a:s", + "timestamp": 1000, + "content": "test", + "event_type": "m.room.message" + }"#; + let doc: ArchiveDocument = serde_json::from_str(json).unwrap(); + assert!(doc.room_name.is_none()); + assert!(doc.sender_name.is_none()); + assert!(doc.reply_to.is_none()); + assert!(doc.thread_id.is_none()); + assert!(doc.media_urls.is_empty()); + assert!(!doc.edited); + assert!(!doc.redacted); + } + + #[test] + fn test_index_mapping_is_valid_json() { + let mapping: serde_json::Value = + serde_json::from_str(index_mapping_json()).unwrap(); + assert!(mapping["settings"]["number_of_shards"].is_number()); + assert!(mapping["mappings"]["properties"]["event_id"]["type"] + .as_str() + .unwrap() + == "keyword"); + assert!(mapping["mappings"]["properties"]["content"]["type"] + .as_str() + .unwrap() + == "text"); + assert!(mapping["mappings"]["properties"]["timestamp"]["type"] + .as_str() + .unwrap() + == "date"); + } +} diff --git a/src/brain/conversation.rs b/src/brain/conversation.rs new file mode 100644 index 0000000..b576d71 --- /dev/null +++ b/src/brain/conversation.rs @@ -0,0 +1,207 @@ +use std::collections::{HashMap, VecDeque}; + +#[derive(Debug, Clone)] +pub struct ContextMessage { + pub sender: String, + pub content: String, + pub timestamp: i64, +} + +struct RoomContext { + messages: VecDeque, + max_size: usize, +} + +impl RoomContext { + fn new(max_size: usize) -> Self { + Self { + messages: VecDeque::with_capacity(max_size), + max_size, + } + } + + fn add(&mut self, msg: ContextMessage) { + if self.messages.len() >= self.max_size { + self.messages.pop_front(); + } + self.messages.push_back(msg); + } + + fn get(&self) -> Vec { + self.messages.iter().cloned().collect() + } +} + +pub struct ConversationManager { + rooms: HashMap, + room_window: usize, + dm_window: usize, + max_rooms: usize, +} + +impl ConversationManager { + pub fn new(room_window: usize, dm_window: usize) -> Self { + Self { + rooms: HashMap::new(), + room_window, + dm_window, + max_rooms: 500, // todo(sienna): make this configurable + } + } + + pub fn add_message(&mut self, room_id: &str, is_dm: bool, msg: ContextMessage) { + let window = if is_dm { + self.dm_window + } else { + self.room_window + }; + + // Evict oldest room if at capacity + if !self.rooms.contains_key(room_id) && self.rooms.len() >= self.max_rooms { + // Remove the room with the oldest latest message + let oldest = self + .rooms + .iter() + .min_by_key(|(_, ctx)| ctx.messages.back().map(|m| m.timestamp).unwrap_or(0)) + .map(|(k, _)| k.to_owned()); + if let Some(key) = oldest { + self.rooms.remove(&key); + } + } + + let ctx = self + .rooms + .entry(room_id.to_owned()) + .or_insert_with(|| RoomContext::new(window)); + ctx.add(msg); + } + + pub fn get_context(&self, room_id: &str) -> Vec { + self.rooms + .get(room_id) + .map(|ctx| ctx.get()) + .unwrap_or_default() + } + + pub fn room_count(&self) -> usize { + self.rooms.len() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn msg(sender: &str, content: &str, ts: i64) -> ContextMessage { + ContextMessage { + sender: sender.to_string(), + content: content.to_string(), + timestamp: ts, + } + } + + #[test] + fn test_add_and_get_messages() { + let mut cm = ConversationManager::new(5, 10); + cm.add_message("!room1:x", false, msg("alice", "hello", 1)); + cm.add_message("!room1:x", false, msg("bob", "hi", 2)); + + let ctx = cm.get_context("!room1:x"); + assert_eq!(ctx.len(), 2); + assert_eq!(ctx[0].sender, "alice"); + assert_eq!(ctx[1].sender, "bob"); + } + + #[test] + fn test_empty_room_returns_empty() { + let cm = ConversationManager::new(5, 10); + let ctx = cm.get_context("!nonexistent:x"); + assert!(ctx.is_empty()); + } + + #[test] + fn test_sliding_window_group_room() { + let mut cm = ConversationManager::new(3, 10); + for i in 0..5 { + cm.add_message("!room:x", false, msg("user", &format!("msg{i}"), i)); + } + let ctx = cm.get_context("!room:x"); + assert_eq!(ctx.len(), 3); + // Should keep the last 3 + assert_eq!(ctx[0].content, "msg2"); + assert_eq!(ctx[1].content, "msg3"); + assert_eq!(ctx[2].content, "msg4"); + } + + #[test] + fn test_sliding_window_dm_room() { + let mut cm = ConversationManager::new(3, 5); + for i in 0..7 { + cm.add_message("!dm:x", true, msg("user", &format!("dm{i}"), i)); + } + let ctx = cm.get_context("!dm:x"); + assert_eq!(ctx.len(), 5); + assert_eq!(ctx[0].content, "dm2"); + assert_eq!(ctx[4].content, "dm6"); + } + + #[test] + fn test_multiple_rooms_independent() { + let mut cm = ConversationManager::new(5, 10); + cm.add_message("!a:x", false, msg("alice", "in room a", 1)); + cm.add_message("!b:x", false, msg("bob", "in room b", 2)); + + assert_eq!(cm.get_context("!a:x").len(), 1); + assert_eq!(cm.get_context("!b:x").len(), 1); + assert_eq!(cm.get_context("!a:x")[0].content, "in room a"); + assert_eq!(cm.get_context("!b:x")[0].content, "in room b"); + } + + #[test] + fn test_lru_eviction_at_max_rooms() { + // Create a manager with max_rooms = 500 (default), but we'll use a small one + let mut cm = ConversationManager::new(5, 10); + cm.max_rooms = 3; + + // Add 3 rooms + cm.add_message("!room1:x", false, msg("a", "r1", 100)); + cm.add_message("!room2:x", false, msg("b", "r2", 200)); + cm.add_message("!room3:x", false, msg("c", "r3", 300)); + assert_eq!(cm.room_count(), 3); + + // Adding a 4th room should evict the one with oldest latest message (room1, ts=100) + cm.add_message("!room4:x", false, msg("d", "r4", 400)); + assert_eq!(cm.room_count(), 3); + assert!(cm.get_context("!room1:x").is_empty()); // evicted + assert_eq!(cm.get_context("!room2:x").len(), 1); + assert_eq!(cm.get_context("!room3:x").len(), 1); + assert_eq!(cm.get_context("!room4:x").len(), 1); + } + + #[test] + fn test_existing_room_not_evicted() { + let mut cm = ConversationManager::new(5, 10); + cm.max_rooms = 2; + + cm.add_message("!room1:x", false, msg("a", "r1", 100)); + cm.add_message("!room2:x", false, msg("b", "r2", 200)); + + // Adding to existing room should NOT trigger eviction + cm.add_message("!room1:x", false, msg("a", "r1 again", 300)); + assert_eq!(cm.room_count(), 2); + assert_eq!(cm.get_context("!room1:x").len(), 2); + } + + #[test] + fn test_message_ordering_preserved() { + let mut cm = ConversationManager::new(10, 10); + cm.add_message("!r:x", false, msg("a", "first", 1)); + cm.add_message("!r:x", false, msg("b", "second", 2)); + cm.add_message("!r:x", false, msg("c", "third", 3)); + + let ctx = cm.get_context("!r:x"); + assert_eq!(ctx[0].timestamp, 1); + assert_eq!(ctx[1].timestamp, 2); + assert_eq!(ctx[2].timestamp, 3); + } +} diff --git a/src/brain/evaluator.rs b/src/brain/evaluator.rs new file mode 100644 index 0000000..9c3423c --- /dev/null +++ b/src/brain/evaluator.rs @@ -0,0 +1,307 @@ +use std::sync::Arc; + +use mistralai_client::v1::{ + chat::{ChatMessage, ChatParams, ResponseFormat}, + constants::Model, +}; +use regex::Regex; +use tracing::{debug, warn}; + +use crate::config::Config; + +#[derive(Debug)] +pub enum Engagement { + MustRespond { reason: MustRespondReason }, + MaybeRespond { relevance: f32, hook: String }, + Ignore, +} + +#[derive(Debug)] +pub enum MustRespondReason { + DirectMention, + DirectMessage, + NameInvocation, +} + +pub struct Evaluator { + config: Arc, + mention_regex: Regex, + name_regex: Regex, +} + +impl Evaluator { + // todo(sienna): regex must be configrable + pub fn new(config: Arc) -> Self { + let user_id = &config.matrix.user_id; + let mention_pattern = regex::escape(user_id); + let mention_regex = Regex::new(&mention_pattern).expect("Failed to compile mention regex"); + let name_regex = + Regex::new(r"(?i)(?:^|\bhey\s+)\bsol\b").expect("Failed to compile name regex"); + + Self { + config, + mention_regex, + name_regex, + } + } + + pub async fn evaluate( + &self, + sender: &str, + body: &str, + is_dm: bool, + recent_messages: &[String], + mistral: &Arc, + ) -> Engagement { + // Don't respond to ourselves + if sender == self.config.matrix.user_id { + return Engagement::Ignore; + } + + // Direct mention: @sol:sunbeam.pt + if self.mention_regex.is_match(body) { + return Engagement::MustRespond { + reason: MustRespondReason::DirectMention, + }; + } + + // DM + if is_dm { + return Engagement::MustRespond { + reason: MustRespondReason::DirectMessage, + }; + } + + // Name invocation: "sol ..." or "hey sol ..." + if self.name_regex.is_match(body) { + return Engagement::MustRespond { + reason: MustRespondReason::NameInvocation, + }; + } + + // Cheap evaluation call for spontaneous responses + self.evaluate_relevance(body, recent_messages, mistral) + .await + } + + /// Check rule-based engagement (without calling Mistral). Returns Some(Engagement) + /// if a rule matched, None if we need to fall through to the LLM evaluation. + pub fn evaluate_rules( + &self, + sender: &str, + body: &str, + is_dm: bool, + ) -> Option { + if sender == self.config.matrix.user_id { + return Some(Engagement::Ignore); + } + if self.mention_regex.is_match(body) { + return Some(Engagement::MustRespond { + reason: MustRespondReason::DirectMention, + }); + } + if is_dm { + return Some(Engagement::MustRespond { + reason: MustRespondReason::DirectMessage, + }); + } + if self.name_regex.is_match(body) { + return Some(Engagement::MustRespond { + reason: MustRespondReason::NameInvocation, + }); + } + None + } + + async fn evaluate_relevance( + &self, + body: &str, + recent_messages: &[String], + mistral: &Arc, + ) -> Engagement { + let context = recent_messages + .iter() + .rev() + .take(5) //todo(sienna): must be configurable + .rev() + .cloned() + .collect::>() + .join("\n"); + + let prompt = format!( + "You are evaluating whether a virtual librarian named Sol should spontaneously join \ + a conversation. Sol has deep knowledge of the group's message archive and helps \ + people find information.\n\n\ + Recent conversation:\n{context}\n\n\ + Latest message: {body}\n\n\ + Respond ONLY with JSON: {{\"relevance\": 0.0-1.0, \"hook\": \"brief reason or empty string\"}}\n\ + relevance=1.0 means Sol absolutely should respond, 0.0 means irrelevant." + ); + + let messages = vec![ChatMessage::new_user_message(&prompt)]; + let params = ChatParams { + response_format: Some(ResponseFormat::json_object()), + temperature: Some(0.1), + max_tokens: Some(100), + ..Default::default() + }; + + let model = Model::new(&self.config.mistral.evaluation_model); + let client = Arc::clone(mistral); + let result = tokio::task::spawn_blocking(move || { + client.chat(model, messages, Some(params)) + }) + .await + .unwrap_or_else(|e| Err(mistralai_client::v1::error::ApiError { + message: format!("spawn_blocking join error: {e}"), + })); + + match result { + Ok(response) => { + let text = &response.choices[0].message.content; + match serde_json::from_str::(text) { + Ok(val) => { + let relevance = val["relevance"].as_f64().unwrap_or(0.0) as f32; + let hook = val["hook"].as_str().unwrap_or("").to_string(); + + debug!(relevance, hook = hook.as_str(), "Evaluation result"); + + if relevance >= self.config.behavior.spontaneous_threshold { + Engagement::MaybeRespond { relevance, hook } + } else { + Engagement::Ignore + } + } + Err(e) => { + warn!("Failed to parse evaluation response: {e}"); + Engagement::Ignore + } + } + } + Err(e) => { + warn!("Evaluation call failed: {e}"); + Engagement::Ignore + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::Config; + + fn test_config() -> Arc { + let toml = r#" +[matrix] +homeserver_url = "https://chat.sunbeam.pt" +user_id = "@sol:sunbeam.pt" +state_store_path = "/tmp/sol" + +[opensearch] +url = "http://localhost:9200" +index = "test" + +[mistral] +[behavior] +"#; + Arc::new(Config::from_str(toml).unwrap()) + } + + fn evaluator() -> Evaluator { + Evaluator::new(test_config()) + } + + #[test] + fn test_ignore_own_messages() { + let ev = evaluator(); + let result = ev.evaluate_rules("@sol:sunbeam.pt", "hello everyone", false); + assert!(matches!(result, Some(Engagement::Ignore))); + } + + #[test] + fn test_direct_mention() { + let ev = evaluator(); + let result = ev.evaluate_rules("@alice:sunbeam.pt", "hey @sol:sunbeam.pt what's up?", false); + assert!(matches!( + result, + Some(Engagement::MustRespond { reason: MustRespondReason::DirectMention }) + )); + } + + #[test] + fn test_dm_detection() { + let ev = evaluator(); + let result = ev.evaluate_rules("@alice:sunbeam.pt", "random message", true); + assert!(matches!( + result, + Some(Engagement::MustRespond { reason: MustRespondReason::DirectMessage }) + )); + } + + #[test] + fn test_name_invocation_start_of_message() { + let ev = evaluator(); + let result = ev.evaluate_rules("@alice:sunbeam.pt", "sol, can you find that link?", false); + assert!(matches!( + result, + Some(Engagement::MustRespond { reason: MustRespondReason::NameInvocation }) + )); + } + + #[test] + fn test_name_invocation_hey_sol() { + let ev = evaluator(); + let result = ev.evaluate_rules("@alice:sunbeam.pt", "hey sol do you remember?", false); + assert!(matches!( + result, + Some(Engagement::MustRespond { reason: MustRespondReason::NameInvocation }) + )); + } + + #[test] + fn test_name_invocation_case_insensitive() { + let ev = evaluator(); + let result = ev.evaluate_rules("@alice:sunbeam.pt", "Hey Sol, help me", false); + assert!(matches!( + result, + Some(Engagement::MustRespond { reason: MustRespondReason::NameInvocation }) + )); + } + + #[test] + fn test_name_invocation_sol_uppercase() { + let ev = evaluator(); + let result = ev.evaluate_rules("@alice:sunbeam.pt", "SOL what do you think?", false); + assert!(matches!( + result, + Some(Engagement::MustRespond { reason: MustRespondReason::NameInvocation }) + )); + } + + #[test] + fn test_no_false_positive_solstice() { + let ev = evaluator(); + // "solstice" should NOT trigger name invocation — \b boundary prevents it + let result = ev.evaluate_rules("@alice:sunbeam.pt", "the solstice is coming", false); + assert!(result.is_none()); + } + + #[test] + fn test_random_message_falls_through() { + let ev = evaluator(); + let result = ev.evaluate_rules("@alice:sunbeam.pt", "what's for lunch?", false); + assert!(result.is_none()); + } + + #[test] + fn test_priority_mention_over_dm() { + // When both mention and DM are true, mention should match first + let ev = evaluator(); + let result = ev.evaluate_rules("@alice:sunbeam.pt", "hi @sol:sunbeam.pt", true); + assert!(matches!( + result, + Some(Engagement::MustRespond { reason: MustRespondReason::DirectMention }) + )); + } +} diff --git a/src/brain/mod.rs b/src/brain/mod.rs new file mode 100644 index 0000000..d8a3e6e --- /dev/null +++ b/src/brain/mod.rs @@ -0,0 +1,4 @@ +pub mod conversation; +pub mod evaluator; +pub mod personality; +pub mod responder; diff --git a/src/brain/personality.rs b/src/brain/personality.rs new file mode 100644 index 0000000..a706afe --- /dev/null +++ b/src/brain/personality.rs @@ -0,0 +1,89 @@ +use chrono::Utc; + +pub struct Personality { + template: String, +} + +impl Personality { + pub fn new(system_prompt: String) -> Self { + Self { + template: system_prompt, + } + } + + pub fn build_system_prompt( + &self, + room_name: &str, + members: &[String], + ) -> String { + let date = Utc::now().format("%Y-%m-%d").to_string(); + let members_str = members.join(", "); + + self.template + .replace("{date}", &date) + .replace("{room_name}", room_name) + .replace("{members}", &members_str) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_date_substitution() { + let p = Personality::new("Today is {date}.".to_string()); + let result = p.build_system_prompt("general", &[]); + let today = Utc::now().format("%Y-%m-%d").to_string(); + assert_eq!(result, format!("Today is {today}.")); + } + + #[test] + fn test_room_name_substitution() { + let p = Personality::new("You are in {room_name}.".to_string()); + let result = p.build_system_prompt("design-chat", &[]); + assert!(result.contains("design-chat")); + } + + #[test] + fn test_members_substitution() { + let p = Personality::new("Members: {members}".to_string()); + let members = vec!["Alice".to_string(), "Bob".to_string(), "Carol".to_string()]; + let result = p.build_system_prompt("room", &members); + assert_eq!(result, "Members: Alice, Bob, Carol"); + } + + #[test] + fn test_empty_members() { + let p = Personality::new("Members: {members}".to_string()); + let result = p.build_system_prompt("room", &[]); + assert_eq!(result, "Members: "); + } + + #[test] + fn test_all_placeholders() { + let template = "Date: {date}, Room: {room_name}, People: {members}".to_string(); + let p = Personality::new(template); + let members = vec!["Sienna".to_string(), "Lonni".to_string()]; + let result = p.build_system_prompt("studio", &members); + + let today = Utc::now().format("%Y-%m-%d").to_string(); + assert!(result.starts_with(&format!("Date: {today}"))); + assert!(result.contains("Room: studio")); + assert!(result.contains("People: Sienna, Lonni")); + } + + #[test] + fn test_no_placeholders_passthrough() { + let p = Personality::new("Static prompt with no variables.".to_string()); + let result = p.build_system_prompt("room", &["Alice".to_string()]); + assert_eq!(result, "Static prompt with no variables."); + } + + #[test] + fn test_multiple_same_placeholder() { + let p = Personality::new("{room_name} is great. I love {room_name}.".to_string()); + let result = p.build_system_prompt("lounge", &[]); + assert_eq!(result, "lounge is great. I love lounge."); + } +} diff --git a/src/brain/responder.rs b/src/brain/responder.rs new file mode 100644 index 0000000..8b24159 --- /dev/null +++ b/src/brain/responder.rs @@ -0,0 +1,179 @@ +use std::sync::Arc; + +use mistralai_client::v1::{ + chat::{ChatMessage, ChatParams, ChatResponse, ChatResponseChoiceFinishReason}, + constants::Model, + error::ApiError, + tool::ToolChoice, +}; +use rand::Rng; +use tokio::time::{sleep, Duration}; +use tracing::{debug, error, info, warn}; + +use crate::brain::conversation::ContextMessage; +use crate::brain::personality::Personality; +use crate::config::Config; +use crate::tools::ToolRegistry; + +/// Run a Mistral chat completion on a blocking thread. +/// +/// The mistral client's `chat_async` holds a `std::sync::MutexGuard` across an +/// `.await` point, making the future !Send. We use the synchronous `chat()` +/// method via `spawn_blocking` instead. +pub(crate) async fn chat_blocking( + client: &Arc, + model: Model, + messages: Vec, + params: ChatParams, +) -> Result { + let client = Arc::clone(client); + tokio::task::spawn_blocking(move || client.chat(model, messages, Some(params))) + .await + .map_err(|e| ApiError { + message: format!("spawn_blocking join error: {e}"), + })? +} + +pub struct Responder { + config: Arc, + personality: Arc, + tools: Arc, +} + +impl Responder { + pub fn new( + config: Arc, + personality: Arc, + tools: Arc, + ) -> Self { + Self { + config, + personality, + tools, + } + } + + pub async fn generate_response( + &self, + context: &[ContextMessage], + trigger_body: &str, + trigger_sender: &str, + room_name: &str, + members: &[String], + is_spontaneous: bool, + mistral: &Arc, + ) -> Option { + // Apply response delay + let delay = if is_spontaneous { + rand::thread_rng().gen_range( + self.config.behavior.spontaneous_delay_min_ms + ..=self.config.behavior.spontaneous_delay_max_ms, + ) + } else { + rand::thread_rng().gen_range( + self.config.behavior.response_delay_min_ms + ..=self.config.behavior.response_delay_max_ms, + ) + }; + sleep(Duration::from_millis(delay)).await; + + let system_prompt = self.personality.build_system_prompt(room_name, members); + + let mut messages = vec![ChatMessage::new_system_message(&system_prompt)]; + + // Add context messages + for msg in context { + if msg.sender == self.config.matrix.user_id { + messages.push(ChatMessage::new_assistant_message(&msg.content, None)); + } else { + let user_msg = format!("{}: {}", msg.sender, msg.content); + messages.push(ChatMessage::new_user_message(&user_msg)); + } + } + + // Add the triggering message + let trigger = format!("{trigger_sender}: {trigger_body}"); + messages.push(ChatMessage::new_user_message(&trigger)); + + let tool_defs = ToolRegistry::tool_definitions(); + let model = Model::new(&self.config.mistral.default_model); + let max_iterations = self.config.mistral.max_tool_iterations; + + for iteration in 0..=max_iterations { + let params = ChatParams { + tools: if iteration < max_iterations { + Some(tool_defs.clone()) + } else { + None + }, + tool_choice: if iteration < max_iterations { + Some(ToolChoice::Auto) + } else { + None + }, + ..Default::default() + }; + + let response = match chat_blocking(mistral, model.clone(), messages.clone(), params).await { + Ok(r) => r, + Err(e) => { + error!("Mistral chat failed: {e}"); + return None; + } + }; + + let choice = &response.choices[0]; + + if choice.finish_reason == ChatResponseChoiceFinishReason::ToolCalls { + if let Some(tool_calls) = &choice.message.tool_calls { + // Add assistant message with tool calls + messages.push(ChatMessage::new_assistant_message( + &choice.message.content, + Some(tool_calls.clone()), + )); + + for tc in tool_calls { + let call_id = tc.id.as_deref().unwrap_or("unknown"); + info!( + tool = tc.function.name.as_str(), + id = call_id, + "Executing tool call" + ); + + let result = self + .tools + .execute(&tc.function.name, &tc.function.arguments) + .await; + + let result_str = match result { + Ok(s) => s, + Err(e) => { + warn!(tool = tc.function.name.as_str(), "Tool failed: {e}"); + format!("Error: {e}") + } + }; + + messages.push(ChatMessage::new_tool_message( + &result_str, + call_id, + Some(&tc.function.name), + )); + } + + debug!(iteration, "Tool iteration complete, continuing"); + continue; + } + } + + // Final text response + let text = choice.message.content.trim().to_string(); + if text.is_empty() { + return None; + } + return Some(text); + } + + warn!("Exceeded max tool iterations"); + None + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..33e57f8 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,219 @@ +use serde::Deserialize; + +#[derive(Debug, Clone, Deserialize)] +pub struct Config { + pub matrix: MatrixConfig, + pub opensearch: OpenSearchConfig, + pub mistral: MistralConfig, + pub behavior: BehaviorConfig, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct MatrixConfig { + pub homeserver_url: String, + pub user_id: String, + pub state_store_path: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct OpenSearchConfig { + pub url: String, + pub index: String, + #[serde(default = "default_batch_size")] + pub batch_size: usize, + #[serde(default = "default_flush_interval_ms")] + pub flush_interval_ms: u64, + #[serde(default = "default_embedding_pipeline")] + pub embedding_pipeline: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct MistralConfig { + #[serde(default = "default_model")] + pub default_model: String, + #[serde(default = "default_evaluation_model")] + pub evaluation_model: String, + #[serde(default = "default_research_model")] + pub research_model: String, + #[serde(default = "default_max_tool_iterations")] + pub max_tool_iterations: usize, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct BehaviorConfig { + #[serde(default = "default_response_delay_min_ms")] + pub response_delay_min_ms: u64, + #[serde(default = "default_response_delay_max_ms")] + pub response_delay_max_ms: u64, + #[serde(default = "default_spontaneous_delay_min_ms")] + pub spontaneous_delay_min_ms: u64, + #[serde(default = "default_spontaneous_delay_max_ms")] + pub spontaneous_delay_max_ms: u64, + #[serde(default = "default_spontaneous_threshold")] + pub spontaneous_threshold: f32, + #[serde(default = "default_room_context_window")] + pub room_context_window: usize, + #[serde(default = "default_dm_context_window")] + pub dm_context_window: usize, + #[serde(default = "default_backfill_on_join")] + pub backfill_on_join: bool, + #[serde(default = "default_backfill_limit")] + pub backfill_limit: usize, +} + +fn default_batch_size() -> usize { 50 } +fn default_flush_interval_ms() -> u64 { 2000 } +fn default_embedding_pipeline() -> String { "tuwunel_embedding_pipeline".into() } +fn default_model() -> String { "mistral-medium-latest".into() } +fn default_evaluation_model() -> String { "ministral-3b-latest".into() } +fn default_research_model() -> String { "mistral-large-latest".into() } +fn default_max_tool_iterations() -> usize { 5 } +fn default_response_delay_min_ms() -> u64 { 2000 } +fn default_response_delay_max_ms() -> u64 { 8000 } +fn default_spontaneous_delay_min_ms() -> u64 { 15000 } +fn default_spontaneous_delay_max_ms() -> u64 { 60000 } +fn default_spontaneous_threshold() -> f32 { 0.7 } +fn default_room_context_window() -> usize { 30 } +fn default_dm_context_window() -> usize { 100 } +fn default_backfill_on_join() -> bool { true } +fn default_backfill_limit() -> usize { 10000 } + +impl Config { + pub fn load(path: &str) -> anyhow::Result { + let content = std::fs::read_to_string(path)?; + let config: Config = toml::from_str(&content)?; + Ok(config) + } + + pub fn from_str(content: &str) -> anyhow::Result { + let config: Config = toml::from_str(content)?; + Ok(config) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const MINIMAL_CONFIG: &str = r#" +[matrix] +homeserver_url = "https://chat.sunbeam.pt" +user_id = "@sol:sunbeam.pt" +state_store_path = "/data/sol/state" + +[opensearch] +url = "http://opensearch:9200" +index = "sol-archive" + +[mistral] + +[behavior] +"#; + + const FULL_CONFIG: &str = r#" +[matrix] +homeserver_url = "https://chat.sunbeam.pt" +user_id = "@sol:sunbeam.pt" +state_store_path = "/data/sol/state" + +[opensearch] +url = "http://opensearch:9200" +index = "sol-archive" +batch_size = 100 +flush_interval_ms = 5000 +embedding_pipeline = "my_pipeline" + +[mistral] +default_model = "mistral-large-latest" +evaluation_model = "ministral-8b-latest" +research_model = "mistral-large-latest" +max_tool_iterations = 10 + +[behavior] +response_delay_min_ms = 1000 +response_delay_max_ms = 5000 +spontaneous_delay_min_ms = 10000 +spontaneous_delay_max_ms = 30000 +spontaneous_threshold = 0.8 +room_context_window = 50 +dm_context_window = 200 +backfill_on_join = false +backfill_limit = 5000 +"#; + + #[test] + fn test_minimal_config_with_defaults() { + let config = Config::from_str(MINIMAL_CONFIG).unwrap(); + + assert_eq!(config.matrix.homeserver_url, "https://chat.sunbeam.pt"); + assert_eq!(config.matrix.user_id, "@sol:sunbeam.pt"); + assert_eq!(config.matrix.state_store_path, "/data/sol/state"); + assert_eq!(config.opensearch.url, "http://opensearch:9200"); + assert_eq!(config.opensearch.index, "sol-archive"); + + // Check defaults + assert_eq!(config.opensearch.batch_size, 50); + assert_eq!(config.opensearch.flush_interval_ms, 2000); + assert_eq!(config.opensearch.embedding_pipeline, "tuwunel_embedding_pipeline"); + assert_eq!(config.mistral.default_model, "mistral-medium-latest"); + assert_eq!(config.mistral.evaluation_model, "ministral-3b-latest"); + assert_eq!(config.mistral.research_model, "mistral-large-latest"); + assert_eq!(config.mistral.max_tool_iterations, 5); + assert_eq!(config.behavior.response_delay_min_ms, 2000); + assert_eq!(config.behavior.response_delay_max_ms, 8000); + assert_eq!(config.behavior.spontaneous_delay_min_ms, 15000); + assert_eq!(config.behavior.spontaneous_delay_max_ms, 60000); + assert!((config.behavior.spontaneous_threshold - 0.7).abs() < f32::EPSILON); + assert_eq!(config.behavior.room_context_window, 30); + assert_eq!(config.behavior.dm_context_window, 100); + assert!(config.behavior.backfill_on_join); + assert_eq!(config.behavior.backfill_limit, 10000); + } + + #[test] + fn test_full_config_overrides() { + let config = Config::from_str(FULL_CONFIG).unwrap(); + + assert_eq!(config.opensearch.batch_size, 100); + assert_eq!(config.opensearch.flush_interval_ms, 5000); + assert_eq!(config.opensearch.embedding_pipeline, "my_pipeline"); + assert_eq!(config.mistral.default_model, "mistral-large-latest"); + assert_eq!(config.mistral.evaluation_model, "ministral-8b-latest"); + assert_eq!(config.mistral.max_tool_iterations, 10); + assert_eq!(config.behavior.response_delay_min_ms, 1000); + assert_eq!(config.behavior.response_delay_max_ms, 5000); + assert!((config.behavior.spontaneous_threshold - 0.8).abs() < f32::EPSILON); + assert_eq!(config.behavior.room_context_window, 50); + assert_eq!(config.behavior.dm_context_window, 200); + assert!(!config.behavior.backfill_on_join); + assert_eq!(config.behavior.backfill_limit, 5000); + } + + #[test] + fn test_missing_required_section_fails() { + let bad = r#" +[matrix] +homeserver_url = "https://chat.sunbeam.pt" +user_id = "@sol:sunbeam.pt" +state_store_path = "/data/sol/state" +"#; + assert!(Config::from_str(bad).is_err()); + } + + #[test] + fn test_missing_required_field_fails() { + let bad = r#" +[matrix] +homeserver_url = "https://chat.sunbeam.pt" +state_store_path = "/data/sol/state" + +[opensearch] +url = "http://opensearch:9200" +index = "sol-archive" + +[mistral] +[behavior] +"#; + assert!(Config::from_str(bad).is_err()); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..37cbdcd --- /dev/null +++ b/src/main.rs @@ -0,0 +1,160 @@ +mod archive; +mod brain; +mod config; +mod matrix_utils; +mod sync; +mod tools; + +use std::sync::Arc; + +use matrix_sdk::Client; +use opensearch::http::transport::TransportBuilder; +use opensearch::OpenSearch; +use ruma::{OwnedDeviceId, OwnedUserId}; +use tokio::signal; +use tokio::sync::Mutex; +use tracing::{error, info}; +use url::Url; + +use archive::indexer::Indexer; +use archive::schema::create_index_if_not_exists; +use brain::conversation::ConversationManager; +use brain::evaluator::Evaluator; +use brain::personality::Personality; +use brain::responder::Responder; +use config::Config; +use sync::AppState; +use tools::ToolRegistry; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Initialize tracing + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("sol=info")), + ) + .init(); + + // Load config + let config_path = + std::env::var("SOL_CONFIG").unwrap_or_else(|_| "/etc/sol/sol.toml".into()); + let config = Config::load(&config_path)?; + info!("Loaded config from {config_path}"); + + // Load system prompt + let prompt_path = std::env::var("SOL_SYSTEM_PROMPT") + .unwrap_or_else(|_| "/etc/sol/system_prompt.md".into()); + let system_prompt = std::fs::read_to_string(&prompt_path)?; + info!("Loaded system prompt from {prompt_path}"); + + // Read secrets from environment + let access_token = std::env::var("SOL_MATRIX_ACCESS_TOKEN") + .map_err(|_| anyhow::anyhow!("SOL_MATRIX_ACCESS_TOKEN not set"))?; + let device_id = std::env::var("SOL_MATRIX_DEVICE_ID") + .map_err(|_| anyhow::anyhow!("SOL_MATRIX_DEVICE_ID not set"))?; + let mistral_api_key = std::env::var("SOL_MISTRAL_API_KEY") + .map_err(|_| anyhow::anyhow!("SOL_MISTRAL_API_KEY not set"))?; + + let config = Arc::new(config); + + // Initialize Matrix client with E2EE and sqlite store + let homeserver = Url::parse(&config.matrix.homeserver_url)?; + + let matrix_client = Client::builder() + .homeserver_url(homeserver) + .sqlite_store(&config.matrix.state_store_path, None) + .build() + .await?; + + // Restore session + let user_id: OwnedUserId = config.matrix.user_id.parse()?; + let device_id: OwnedDeviceId = device_id.into(); + + let session = matrix_sdk::AuthSession::Matrix(matrix_sdk::matrix_auth::MatrixSession { + meta: matrix_sdk::SessionMeta { + user_id, + device_id, + }, + tokens: matrix_sdk::matrix_auth::MatrixSessionTokens { + access_token, + refresh_token: None, + }, + }); + + matrix_client.restore_session(session).await?; + info!(user = %config.matrix.user_id, "Matrix session restored"); + + // Initialize OpenSearch client + let os_url = Url::parse(&config.opensearch.url)?; + let os_transport = TransportBuilder::new( + opensearch::http::transport::SingleNodeConnectionPool::new(os_url), + ) + .build()?; + let os_client = OpenSearch::new(os_transport); + + // Ensure index exists + create_index_if_not_exists(&os_client, &config.opensearch.index).await?; + + // Initialize Mistral client + let mistral_client = mistralai_client::v1::client::Client::new( + Some(mistral_api_key), + None, + None, + None, + )?; + let mistral = Arc::new(mistral_client); + + // Build components + let personality = Arc::new(Personality::new(system_prompt)); + let tool_registry = Arc::new(ToolRegistry::new( + os_client.clone(), + matrix_client.clone(), + config.clone(), + )); + let indexer = Arc::new(Indexer::new(os_client, config.clone())); + let evaluator = Arc::new(Evaluator::new(config.clone())); + let responder = Arc::new(Responder::new( + config.clone(), + personality, + tool_registry, + )); + let conversations = Arc::new(Mutex::new(ConversationManager::new( + config.behavior.room_context_window, + config.behavior.dm_context_window, + ))); + + // Start background flush task + let _flush_handle = indexer.start_flush_task(); + + // Build shared state + let state = Arc::new(AppState { + config: config.clone(), + indexer, + evaluator, + responder, + conversations, + mistral, + }); + + // Start sync loop in background + let sync_client = matrix_client.clone(); + let sync_state = state.clone(); + let sync_handle = tokio::spawn(async move { + if let Err(e) = sync::start_sync(sync_client, sync_state).await { + error!("Sync loop error: {e}"); + } + }); + + info!("Sol is running"); + + // Wait for shutdown signal + signal::ctrl_c().await?; + info!("Shutdown signal received"); + + // Cancel sync + sync_handle.abort(); + + info!("Sol has shut down"); + Ok(()) +} diff --git a/src/matrix_utils.rs b/src/matrix_utils.rs new file mode 100644 index 0000000..9a294ec --- /dev/null +++ b/src/matrix_utils.rs @@ -0,0 +1,77 @@ +use matrix_sdk::room::Room; +use matrix_sdk::RoomMemberships; +use ruma::events::room::message::{ + MessageType, OriginalSyncRoomMessageEvent, Relation, RoomMessageEventContent, +}; +use ruma::events::relation::InReplyTo; +use ruma::OwnedEventId; + +/// Extract the plain-text body from a message event. +pub fn extract_body(event: &OriginalSyncRoomMessageEvent) -> Option { + match &event.content.msgtype { + MessageType::Text(text) => Some(text.body.clone()), + MessageType::Notice(notice) => Some(notice.body.clone()), + MessageType::Emote(emote) => Some(emote.body.clone()), + _ => None, + } +} + +/// Check if this event is an edit (m.replace relation) and return the new body. +pub fn extract_edit(event: &OriginalSyncRoomMessageEvent) -> Option<(OwnedEventId, String)> { + if let Some(Relation::Replacement(replacement)) = &event.content.relates_to { + let new_body = match &replacement.new_content.msgtype { + MessageType::Text(text) => text.body.clone(), + MessageType::Notice(notice) => notice.body.clone(), + _ => return None, + }; + return Some((replacement.event_id.clone(), new_body)); + } + None +} + +/// Extract the event ID being replied to, if any. +pub fn extract_reply_to(event: &OriginalSyncRoomMessageEvent) -> Option { + if let Some(Relation::Reply { in_reply_to }) = &event.content.relates_to { + return Some(in_reply_to.event_id.clone()); + } + None +} + +/// Extract thread root event ID, if any. +pub fn extract_thread_id(event: &OriginalSyncRoomMessageEvent) -> Option { + if let Some(Relation::Thread(thread)) = &event.content.relates_to { + return Some(thread.event_id.clone()); + } + None +} + +/// Build a reply message content with m.in_reply_to relation. +pub fn make_reply_content(body: &str, reply_to_event_id: OwnedEventId) -> RoomMessageEventContent { + let mut content = RoomMessageEventContent::text_plain(body); + content.relates_to = Some(Relation::Reply { + in_reply_to: InReplyTo::new(reply_to_event_id), + }); + content +} + +/// Get the display name for a room. +pub fn room_display_name(room: &Room) -> String { + room.cached_display_name() + .map(|n| n.to_string()) + .unwrap_or_else(|| room.room_id().to_string()) +} + +/// Get member display names for a room. +pub async fn room_member_names(room: &Room) -> Vec { + match room.members(RoomMemberships::JOIN).await { + Ok(members) => members + .iter() + .map(|m| { + m.display_name() + .unwrap_or_else(|| m.user_id().as_str()) + .to_string() + }) + .collect(), + Err(_) => Vec::new(), + } +} diff --git a/src/sync.rs b/src/sync.rs new file mode 100644 index 0000000..457bdf2 --- /dev/null +++ b/src/sync.rs @@ -0,0 +1,228 @@ +use std::sync::Arc; + +use matrix_sdk::config::SyncSettings; +use matrix_sdk::room::Room; +use matrix_sdk::Client; +use ruma::events::room::member::StrippedRoomMemberEvent; +use ruma::events::room::message::OriginalSyncRoomMessageEvent; +use ruma::events::room::redaction::OriginalSyncRoomRedactionEvent; +use tokio::sync::Mutex; +use tracing::{error, info, warn}; + +use crate::archive::indexer::Indexer; +use crate::archive::schema::ArchiveDocument; +use crate::brain::conversation::{ContextMessage, ConversationManager}; +use crate::brain::evaluator::{Engagement, Evaluator}; +use crate::brain::responder::Responder; +use crate::config::Config; +use crate::matrix_utils; + +pub struct AppState { + pub config: Arc, + pub indexer: Arc, + pub evaluator: Arc, + pub responder: Arc, + pub conversations: Arc>, + pub mistral: Arc, +} + +pub async fn start_sync(client: Client, state: Arc) -> anyhow::Result<()> { + // Register event handlers + let s = state.clone(); + client.add_event_handler( + move |event: OriginalSyncRoomMessageEvent, room: Room| { + let state = s.clone(); + async move { + if let Err(e) = handle_message(event, room, state).await { + error!("Error handling message: {e}"); + } + } + }, + ); + + let s = state.clone(); + client.add_event_handler( + move |event: OriginalSyncRoomRedactionEvent, _room: Room| { + let state = s.clone(); + async move { + handle_redaction(event, &state).await; + } + }, + ); + + client.add_event_handler( + move |event: StrippedRoomMemberEvent, room: Room| async move { + handle_invite(event, room).await; + }, + ); + + info!("Starting Matrix sync loop"); + let settings = SyncSettings::default(); + client.sync(settings).await?; + + Ok(()) +} + +async fn handle_message( + event: OriginalSyncRoomMessageEvent, + room: Room, + state: Arc, +) -> anyhow::Result<()> { + let sender = event.sender.to_string(); + let room_id = room.room_id().to_string(); + let event_id = event.event_id.to_string(); + let timestamp = event.origin_server_ts.0.into(); + + // Check if this is an edit + if let Some((original_id, new_body)) = matrix_utils::extract_edit(&event) { + state + .indexer + .update_edit(&original_id.to_string(), &new_body) + .await; + return Ok(()); + } + + let Some(body) = matrix_utils::extract_body(&event) else { + return Ok(()); + }; + + let room_name = matrix_utils::room_display_name(&room); + let sender_name = room + .get_member_no_sync(&event.sender) + .await + .ok() + .flatten() + .and_then(|m| m.display_name().map(|s| s.to_string())); + + let reply_to = matrix_utils::extract_reply_to(&event).map(|id| id.to_string()); + let thread_id = matrix_utils::extract_thread_id(&event).map(|id| id.to_string()); + + // Archive the message + let doc = ArchiveDocument { + event_id: event_id.clone(), + room_id: room_id.clone(), + room_name: Some(room_name.clone()), + sender: sender.clone(), + sender_name: sender_name.clone(), + timestamp, + content: body.clone(), + reply_to, + thread_id, + media_urls: Vec::new(), + event_type: "m.room.message".into(), + edited: false, + redacted: false, + }; + state.indexer.add(doc).await; + + // Update conversation context + let is_dm = room.is_direct().await.unwrap_or(false); + { + let mut convs = state.conversations.lock().await; + convs.add_message( + &room_id, + is_dm, + ContextMessage { + sender: sender_name.clone().unwrap_or_else(|| sender.clone()), + content: body.clone(), + timestamp, + }, + ); + } + + // Evaluate whether to respond + let recent: Vec = { + let convs = state.conversations.lock().await; + convs + .get_context(&room_id) + .iter() + .map(|m| format!("{}: {}", m.sender, m.content)) + .collect() + }; + + let engagement = state + .evaluator + .evaluate(&sender, &body, is_dm, &recent, &state.mistral) + .await; + + let (should_respond, is_spontaneous) = match engagement { + Engagement::MustRespond { reason } => { + info!(?reason, "Must respond"); + (true, false) + } + Engagement::MaybeRespond { relevance, hook } => { + info!(relevance, hook = hook.as_str(), "Maybe respond (spontaneous)"); + (true, true) + } + Engagement::Ignore => (false, false), + }; + + if !should_respond { + return Ok(()); + } + + // Show typing indicator + let _ = room.typing_notice(true).await; + + let context = { + let convs = state.conversations.lock().await; + convs.get_context(&room_id) + }; + let members = matrix_utils::room_member_names(&room).await; + let display_sender = sender_name.as_deref().unwrap_or(&sender); + + let response = state + .responder + .generate_response( + &context, + &body, + display_sender, + &room_name, + &members, + is_spontaneous, + &state.mistral, + ) + .await; + + // Stop typing indicator + let _ = room.typing_notice(false).await; + + if let Some(text) = response { + let content = matrix_utils::make_reply_content(&text, event.event_id.to_owned()); + if let Err(e) = room.send(content).await { + error!("Failed to send response: {e}"); + } + } + + Ok(()) +} + +async fn handle_redaction(event: OriginalSyncRoomRedactionEvent, state: &AppState) { + if let Some(redacted_id) = &event.redacts { + state.indexer.update_redaction(&redacted_id.to_string()).await; + } +} + +async fn handle_invite(event: StrippedRoomMemberEvent, room: Room) { + // Only handle our own invites + if event.state_key != room.own_user_id() { + return; + } + + info!(room_id = %room.room_id(), "Received invite, auto-joining"); + tokio::spawn(async move { + for attempt in 0..3u32 { + match room.join().await { + Ok(_) => { + info!(room_id = %room.room_id(), "Joined room"); + return; + } + Err(e) => { + warn!(room_id = %room.room_id(), attempt, "Failed to join: {e}"); + tokio::time::sleep(std::time::Duration::from_secs(2u64.pow(attempt))).await; + } + } + } + error!(room_id = %room.room_id(), "Failed to join after retries"); + }); +} diff --git a/src/tools/mod.rs b/src/tools/mod.rs new file mode 100644 index 0000000..3661f04 --- /dev/null +++ b/src/tools/mod.rs @@ -0,0 +1,151 @@ +pub mod room_history; +pub mod room_info; +pub mod search; + +use std::sync::Arc; + +use matrix_sdk::Client as MatrixClient; +use mistralai_client::v1::tool::Tool; +use opensearch::OpenSearch; +use serde_json::json; + +use crate::config::Config; + +pub struct ToolRegistry { + opensearch: OpenSearch, + matrix: MatrixClient, + config: Arc, +} + +impl ToolRegistry { + pub fn new(opensearch: OpenSearch, matrix: MatrixClient, config: Arc) -> Self { + Self { + opensearch, + matrix, + config, + } + } + + pub fn tool_definitions() -> Vec { + vec![ + Tool::new( + "search_archive".into(), + "Search the message archive. Use this to find past conversations, \ + messages from specific people, or about specific topics." + .into(), + json!({ + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Search query for message content" + }, + "room": { + "type": "string", + "description": "Filter by room name (optional)" + }, + "sender": { + "type": "string", + "description": "Filter by sender display name (optional)" + }, + "after": { + "type": "string", + "description": "Unix timestamp in ms — only messages after this time (optional)" + }, + "before": { + "type": "string", + "description": "Unix timestamp in ms — only messages before this time (optional)" + }, + "limit": { + "type": "integer", + "description": "Max results to return (default 10)" + }, + "semantic": { + "type": "boolean", + "description": "Use semantic search instead of keyword (optional)" + } + }, + "required": ["query"] + }), + ), + Tool::new( + "get_room_context".into(), + "Get messages around a specific point in time or event in a room. \ + Useful for understanding the context of a conversation." + .into(), + json!({ + "type": "object", + "properties": { + "room_id": { + "type": "string", + "description": "The Matrix room ID" + }, + "around_timestamp": { + "type": "integer", + "description": "Unix timestamp in ms to center the context around" + }, + "around_event_id": { + "type": "string", + "description": "Event ID to center the context around" + }, + "before_count": { + "type": "integer", + "description": "Number of messages before the pivot (default 10)" + }, + "after_count": { + "type": "integer", + "description": "Number of messages after the pivot (default 10)" + } + }, + "required": ["room_id"] + }), + ), + Tool::new( + "list_rooms".into(), + "List all rooms Sol is currently in, with names and member counts.".into(), + json!({ + "type": "object", + "properties": {} + }), + ), + Tool::new( + "get_room_members".into(), + "Get the list of members in a specific room.".into(), + json!({ + "type": "object", + "properties": { + "room_id": { + "type": "string", + "description": "The Matrix room ID" + } + }, + "required": ["room_id"] + }), + ), + ] + } + + pub async fn execute(&self, name: &str, arguments: &str) -> anyhow::Result { + match name { + "search_archive" => { + search::search_archive( + &self.opensearch, + &self.config.opensearch.index, + arguments, + ) + .await + } + "get_room_context" => { + room_history::get_room_context( + &self.opensearch, + &self.config.opensearch.index, + arguments, + ) + .await + } + "list_rooms" => room_info::list_rooms(&self.matrix).await, + "get_room_members" => room_info::get_room_members(&self.matrix, arguments).await, + _ => anyhow::bail!("Unknown tool: {name}"), + } + } +} diff --git a/src/tools/room_history.rs b/src/tools/room_history.rs new file mode 100644 index 0000000..d82131c --- /dev/null +++ b/src/tools/room_history.rs @@ -0,0 +1,108 @@ +use opensearch::OpenSearch; +use serde::Deserialize; +use serde_json::json; + +#[derive(Debug, Deserialize)] +pub struct RoomHistoryArgs { + pub room_id: String, + #[serde(default)] + pub around_timestamp: Option, + #[serde(default)] + pub around_event_id: Option, + #[serde(default = "default_count")] + pub before_count: usize, + #[serde(default = "default_count")] + pub after_count: usize, +} + +fn default_count() -> usize { 10 } + +pub async fn get_room_context( + client: &OpenSearch, + index: &str, + args_json: &str, +) -> anyhow::Result { + let args: RoomHistoryArgs = serde_json::from_str(args_json)?; + let total = args.before_count + args.after_count + 1; + + // Determine the pivot timestamp + let pivot_ts = if let Some(ts) = args.around_timestamp { + ts + } else if let Some(ref event_id) = args.around_event_id { + // Look up the event to get its timestamp + let lookup = json!({ + "size": 1, + "query": { "term": { "event_id": event_id } }, + "_source": ["timestamp"] + }); + + let resp = client + .search(opensearch::SearchParts::Index(&[index])) + .body(lookup) + .send() + .await?; + + let body: serde_json::Value = resp.json().await?; + body["hits"]["hits"][0]["_source"]["timestamp"] + .as_i64() + .ok_or_else(|| anyhow::anyhow!("Event {event_id} not found in archive"))? + } else { + anyhow::bail!("Either around_timestamp or around_event_id must be provided"); + }; + + let query = json!({ + "size": total, + "query": { + "bool": { + "must": [ + { "term": { "room_id": args.room_id } }, + { "term": { "redacted": false } } + ], + "should": [ + { + "range": { + "timestamp": { + "gte": pivot_ts - 3_600_000, + "lte": pivot_ts + 3_600_000 + } + } + } + ] + } + }, + "sort": [{ "timestamp": "asc" }] + }); + + let response = client + .search(opensearch::SearchParts::Index(&[index])) + .body(query) + .send() + .await?; + + let body: serde_json::Value = response.json().await?; + let hits = &body["hits"]["hits"]; + + let Some(hits_arr) = hits.as_array() else { + return Ok("No messages found around that point.".into()); + }; + + if hits_arr.is_empty() { + return Ok("No messages found around that point.".into()); + } + + let mut output = String::new(); + for hit in hits_arr { + let src = &hit["_source"]; + let sender = src["sender_name"].as_str().unwrap_or("unknown"); + let content = src["content"].as_str().unwrap_or(""); + let ts = src["timestamp"].as_i64().unwrap_or(0); + + let dt = chrono::DateTime::from_timestamp_millis(ts) + .map(|d| d.format("%H:%M").to_string()) + .unwrap_or_else(|| "??:??".into()); + + output.push_str(&format!("[{dt}] {sender}: {content}\n")); + } + + Ok(output) +} diff --git a/src/tools/room_info.rs b/src/tools/room_info.rs new file mode 100644 index 0000000..bf4781c --- /dev/null +++ b/src/tools/room_info.rs @@ -0,0 +1,55 @@ +use matrix_sdk::Client; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct ListRoomsArgs {} + +#[derive(Debug, Deserialize)] +pub struct GetMembersArgs { + pub room_id: String, +} + +pub async fn list_rooms(client: &Client) -> anyhow::Result { + let rooms = client.joined_rooms(); + if rooms.is_empty() { + return Ok("I'm not in any rooms.".into()); + } + + let mut output = String::new(); + for room in &rooms { + let name = match room.cached_display_name() { + Some(n) => n.to_string(), + None => room.room_id().to_string(), + }; + let id = room.room_id(); + let members = room.joined_members_count(); + output.push_str(&format!("- {name} ({id}) — {members} members\n")); + } + + Ok(output) +} + +pub async fn get_room_members(client: &Client, args_json: &str) -> anyhow::Result { + let args: GetMembersArgs = serde_json::from_str(args_json)?; + let room_id = <&ruma::RoomId>::try_from(args.room_id.as_str())?; + + let Some(room) = client.get_room(room_id) else { + anyhow::bail!("I'm not in room {}", args.room_id); + }; + + let members = room.members(matrix_sdk::RoomMemberships::JOIN).await?; + if members.is_empty() { + return Ok("No members found.".into()); + } + + let mut output = String::new(); + for member in &members { + let display = member + .display_name() + .unwrap_or_else(|| member.user_id().as_str()); + let user_id = member.user_id(); + output.push_str(&format!("- {display} ({user_id})\n")); + } + + Ok(output) +} diff --git a/src/tools/search.rs b/src/tools/search.rs new file mode 100644 index 0000000..83ca21f --- /dev/null +++ b/src/tools/search.rs @@ -0,0 +1,274 @@ +use opensearch::OpenSearch; +use serde::Deserialize; +use serde_json::json; +use tracing::debug; + +#[derive(Debug, Deserialize)] +pub struct SearchArgs { + pub query: String, + #[serde(default)] + pub room: Option, + #[serde(default)] + pub sender: Option, + #[serde(default)] + pub after: Option, + #[serde(default)] + pub before: Option, + #[serde(default = "default_limit")] + pub limit: usize, + #[serde(default)] + pub semantic: Option, +} + +fn default_limit() -> usize { 10 } + +/// Build the OpenSearch query body from parsed SearchArgs. Extracted for testability. +pub fn build_search_query(args: &SearchArgs) -> serde_json::Value { + let must = vec![json!({ + "match": { "content": args.query } + })]; + + let mut filter = vec![json!({ + "term": { "redacted": false } + })]; + + if let Some(ref room) = args.room { + filter.push(json!({ "term": { "room_name": room } })); + } + if let Some(ref sender) = args.sender { + filter.push(json!({ "term": { "sender_name": sender } })); + } + + let mut range = serde_json::Map::new(); + if let Some(ref after) = args.after { + if let Ok(ts) = after.parse::() { + range.insert("gte".into(), json!(ts)); + } + } + if let Some(ref before) = args.before { + if let Ok(ts) = before.parse::() { + range.insert("lte".into(), json!(ts)); + } + } + if !range.is_empty() { + filter.push(json!({ "range": { "timestamp": range } })); + } + + json!({ + "size": args.limit, + "query": { + "bool": { + "must": must, + "filter": filter + } + }, + "sort": [{ "timestamp": "desc" }], + "_source": ["event_id", "room_name", "sender_name", "timestamp", "content"] + }) +} + +pub async fn search_archive( + client: &OpenSearch, + index: &str, + args_json: &str, +) -> anyhow::Result { + let args: SearchArgs = serde_json::from_str(args_json)?; + debug!(query = args.query.as_str(), "Searching archive"); + + let query_body = build_search_query(&args); + + let response = client + .search(opensearch::SearchParts::Index(&[index])) + .body(query_body) + .send() + .await?; + + let body: serde_json::Value = response.json().await?; + let hits = &body["hits"]["hits"]; + + let Some(hits_arr) = hits.as_array() else { + return Ok("No results found.".into()); + }; + + if hits_arr.is_empty() { + return Ok("No results found.".into()); + } + + let mut output = String::new(); + for (i, hit) in hits_arr.iter().enumerate() { + let src = &hit["_source"]; + let sender = src["sender_name"].as_str().unwrap_or("unknown"); + let room = src["room_name"].as_str().unwrap_or("unknown"); + let content = src["content"].as_str().unwrap_or(""); + let ts = src["timestamp"].as_i64().unwrap_or(0); + + let dt = chrono::DateTime::from_timestamp_millis(ts) + .map(|d| d.format("%Y-%m-%d %H:%M").to_string()) + .unwrap_or_else(|| "unknown date".into()); + + output.push_str(&format!( + "{}. [{dt}] #{room} — {sender}: {content}\n", + i + 1 + )); + } + + Ok(output) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn parse_args(json: &str) -> SearchArgs { + serde_json::from_str(json).unwrap() + } + + #[test] + fn test_parse_minimal_args() { + let args = parse_args(r#"{"query": "hello"}"#); + assert_eq!(args.query, "hello"); + assert!(args.room.is_none()); + assert!(args.sender.is_none()); + assert!(args.after.is_none()); + assert!(args.before.is_none()); + assert_eq!(args.limit, 10); // default + assert!(args.semantic.is_none()); + } + + #[test] + fn test_parse_full_args() { + let args = parse_args(r#"{ + "query": "meeting notes", + "room": "general", + "sender": "Alice", + "after": "1710000000000", + "before": "1710100000000", + "limit": 25, + "semantic": true + }"#); + assert_eq!(args.query, "meeting notes"); + assert_eq!(args.room.as_deref(), Some("general")); + assert_eq!(args.sender.as_deref(), Some("Alice")); + assert_eq!(args.after.as_deref(), Some("1710000000000")); + assert_eq!(args.before.as_deref(), Some("1710100000000")); + assert_eq!(args.limit, 25); + assert_eq!(args.semantic, Some(true)); + } + + #[test] + fn test_query_basic() { + let args = parse_args(r#"{"query": "test"}"#); + let q = build_search_query(&args); + + assert_eq!(q["size"], 10); + assert_eq!(q["query"]["bool"]["must"][0]["match"]["content"], "test"); + assert_eq!(q["query"]["bool"]["filter"][0]["term"]["redacted"], false); + assert_eq!(q["sort"][0]["timestamp"], "desc"); + } + + #[test] + fn test_query_with_room_filter() { + let args = parse_args(r#"{"query": "hello", "room": "design"}"#); + let q = build_search_query(&args); + + let filters = q["query"]["bool"]["filter"].as_array().unwrap(); + assert_eq!(filters.len(), 2); + assert_eq!(filters[1]["term"]["room_name"], "design"); + } + + #[test] + fn test_query_with_sender_filter() { + let args = parse_args(r#"{"query": "hello", "sender": "Bob"}"#); + let q = build_search_query(&args); + + let filters = q["query"]["bool"]["filter"].as_array().unwrap(); + assert_eq!(filters.len(), 2); + assert_eq!(filters[1]["term"]["sender_name"], "Bob"); + } + + #[test] + fn test_query_with_room_and_sender() { + let args = parse_args(r#"{"query": "hello", "room": "dev", "sender": "Carol"}"#); + let q = build_search_query(&args); + + let filters = q["query"]["bool"]["filter"].as_array().unwrap(); + assert_eq!(filters.len(), 3); + assert_eq!(filters[1]["term"]["room_name"], "dev"); + assert_eq!(filters[2]["term"]["sender_name"], "Carol"); + } + + #[test] + fn test_query_with_date_range() { + let args = parse_args(r#"{ + "query": "hello", + "after": "1710000000000", + "before": "1710100000000" + }"#); + let q = build_search_query(&args); + + let filters = q["query"]["bool"]["filter"].as_array().unwrap(); + let range_filter = &filters[1]["range"]["timestamp"]; + assert_eq!(range_filter["gte"], 1710000000000_i64); + assert_eq!(range_filter["lte"], 1710100000000_i64); + } + + #[test] + fn test_query_with_after_only() { + let args = parse_args(r#"{"query": "hello", "after": "1710000000000"}"#); + let q = build_search_query(&args); + + let filters = q["query"]["bool"]["filter"].as_array().unwrap(); + let range_filter = &filters[1]["range"]["timestamp"]; + assert_eq!(range_filter["gte"], 1710000000000_i64); + assert!(range_filter.get("lte").is_none()); + } + + #[test] + fn test_query_with_custom_limit() { + let args = parse_args(r#"{"query": "hello", "limit": 50}"#); + let q = build_search_query(&args); + assert_eq!(q["size"], 50); + } + + #[test] + fn test_query_all_filters_combined() { + let args = parse_args(r#"{ + "query": "architecture", + "room": "engineering", + "sender": "Sienna", + "after": "1000", + "before": "2000", + "limit": 5 + }"#); + let q = build_search_query(&args); + + assert_eq!(q["size"], 5); + let filters = q["query"]["bool"]["filter"].as_array().unwrap(); + // redacted=false, room, sender, range = 4 filters + assert_eq!(filters.len(), 4); + } + + #[test] + fn test_invalid_timestamp_ignored() { + let args = parse_args(r#"{"query": "hello", "after": "not-a-number"}"#); + let q = build_search_query(&args); + + let filters = q["query"]["bool"]["filter"].as_array().unwrap(); + // Only the redacted filter, no range since parse failed + assert_eq!(filters.len(), 1); + } + + #[test] + fn test_source_fields() { + let args = parse_args(r#"{"query": "test"}"#); + let q = build_search_query(&args); + + let source = q["_source"].as_array().unwrap(); + let fields: Vec<&str> = source.iter().map(|v| v.as_str().unwrap()).collect(); + assert!(fields.contains(&"event_id")); + assert!(fields.contains(&"room_name")); + assert!(fields.contains(&"sender_name")); + assert!(fields.contains(&"timestamp")); + assert!(fields.contains(&"content")); + } +}