diff --git a/Cargo.lock b/Cargo.lock index 835ca5f393..0a9f3634b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6567,7 +6567,7 @@ dependencies = [ [[package]] name = "tinymemory" -version = "0.1.0" +version = "0.3.0" dependencies = [ "anyhow", "async-trait", diff --git a/app/src-tauri/Cargo.lock b/app/src-tauri/Cargo.lock index d45c4f4a92..314b74fced 100644 --- a/app/src-tauri/Cargo.lock +++ b/app/src-tauri/Cargo.lock @@ -53,7 +53,7 @@ dependencies = [ "tokio-util", "toml 0.8.2", "url", - "uuid 1.23.1", + "uuid", "windows-sys 0.59.0", ] @@ -72,15 +72,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "adobe-cmap-parser" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae8abfa9a4688de8fc9f42b3f013b6fffec18ed8a554f5f113577e0b9b3212a3" -dependencies = [ - "pom", -] - [[package]] name = "aead" version = "0.5.2" @@ -251,7 +242,7 @@ dependencies = [ "objc2-foundation 0.3.2", "parking_lot", "percent-encoding", - "windows-sys 0.60.2", + "windows-sys 0.59.0", "x11rb", ] @@ -264,7 +255,7 @@ dependencies = [ "base64ct", "blake2", "cpufeatures 0.2.17", - "password-hash 0.5.0", + "password-hash", ] [[package]] @@ -506,17 +497,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "auto_impl" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "autocfg" version = "1.5.0" @@ -611,16 +591,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" -[[package]] -name = "base58ck" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" -dependencies = [ - "bitcoin-internals", - "bitcoin_hashes", -] - [[package]] name = "base64" version = "0.21.7" @@ -633,6 +603,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "base64ct" version = "1.8.3" @@ -684,54 +660,6 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" -[[package]] -name = "bitcoin" -version = "0.32.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf93e61f2dbc3e3c41234ca26a65e2c0b0975c52e0f069ab9893ebbede584d3" -dependencies = [ - "base58ck", - "bech32 0.11.1", - "bitcoin-internals", - "bitcoin-io", - "bitcoin-units", - "bitcoin_hashes", - "hex-conservative", - "hex_lit", - "secp256k1", -] - -[[package]] -name = "bitcoin-internals" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" - -[[package]] -name = "bitcoin-io" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" - -[[package]] -name = "bitcoin-units" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346568ebaab2918487cea76dd55dae13c27bb618cdb737c952e69eb2017c4118" -dependencies = [ - "bitcoin-internals", -] - -[[package]] -name = "bitcoin_hashes" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" -dependencies = [ - "bitcoin-io", - "hex-conservative", -] - [[package]] name = "bitflags" version = "1.3.2" @@ -859,18 +787,6 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" -[[package]] -name = "byte-slice-cast" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" - -[[package]] -name = "bytecount" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" - [[package]] name = "bytemuck" version = "1.25.0" @@ -898,26 +814,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bzip2" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" -dependencies = [ - "bzip2-sys", - "libc", -] - -[[package]] -name = "bzip2-sys" -version = "0.1.13+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" -dependencies = [ - "cc", - "pkg-config", -] - [[package]] name = "cairo-rs" version = "0.18.5" @@ -1029,15 +925,9 @@ checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" dependencies = [ "byteorder", "fnv", - "uuid 1.23.1", + "uuid", ] -[[package]] -name = "cff-parser" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f5b6e9141c036f3ff4ce7b2f7e432b0f00dee416ddcd4f17741d189ddc2e9d" - [[package]] name = "cfg-expr" version = "0.15.8" @@ -1191,7 +1081,7 @@ dependencies = [ "coins-bip32", "hmac", "once_cell", - "pbkdf2 0.12.2", + "pbkdf2", "rand 0.8.6", "sha2 0.10.9", "thiserror 1.0.69", @@ -1217,12 +1107,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - [[package]] name = "colorchoice" version = "1.0.5" @@ -1264,18 +1148,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "const-hex" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20d9a563d167a9cce0f94153382b33cb6eded6dfabff03c69ad65a28ea1514e0" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "proptest", - "serde_core", -] - [[package]] name = "const-oid" version = "0.9.6" @@ -1308,33 +1180,6 @@ dependencies = [ "tiny-keccak", ] -[[package]] -name = "const_format" -version = "0.2.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" -dependencies = [ - "const_format_proc_macros", - "konst", -] - -[[package]] -name = "const_format_proc_macros" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "constant_time_eq" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" - [[package]] name = "cookie" version = "0.18.1" @@ -1736,7 +1581,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" dependencies = [ "serde", - "uuid 1.23.1", + "uuid", ] [[package]] @@ -1770,33 +1615,13 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "derive_more" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" -dependencies = [ - "derive_more-impl 1.0.0", -] - [[package]] name = "derive_more" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ - "derive_more-impl 2.1.1", -] - -[[package]] -name = "derive_more-impl" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "derive_more-impl", ] [[package]] @@ -1891,7 +1716,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1923,7 +1748,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" dependencies = [ - "libloading 0.8.9", + "libloading 0.7.4", ] [[package]] @@ -1958,21 +1783,6 @@ dependencies = [ "const-random", ] -[[package]] -name = "docx-rs" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed73cbf5e1c37baa23f4132569ac1187829f03922c206bd68fe109e3001a343d" -dependencies = [ - "base64 0.22.1", - "image", - "quick-xml 0.36.2", - "serde", - "serde_json", - "thiserror 2.0.18", - "zip 0.6.6", -] - [[package]] name = "dom_query" version = "0.27.0" @@ -2051,15 +1861,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" -[[package]] -name = "ecb" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a8bfa975b1aec2145850fcaa1c6fe269a16578c44705a532ae3edc92b8881c7" -dependencies = [ - "cipher", -] - [[package]] name = "ecdsa" version = "0.16.9" @@ -2160,15 +1961,6 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" -[[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 = "endi" version = "1.1.1" @@ -2289,7 +2081,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2298,131 +2090,6 @@ version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" -[[package]] -name = "eth-keystore" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fda3bf123be441da5260717e0661c25a2fd9cb2b2c1d20bf2e05580047158ab" -dependencies = [ - "aes", - "ctr", - "digest 0.10.7", - "hex", - "hmac", - "pbkdf2 0.11.0", - "rand 0.8.6", - "scrypt", - "serde", - "serde_json", - "sha2 0.10.9", - "sha3", - "thiserror 1.0.69", - "uuid 0.8.2", -] - -[[package]] -name = "ethabi" -version = "18.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7413c5f74cc903ea37386a8965a936cbeb334bd270862fdece542c1b2dcbc898" -dependencies = [ - "ethereum-types", - "hex", - "once_cell", - "regex", - "serde", - "serde_json", - "sha3", - "thiserror 1.0.69", - "uint", -] - -[[package]] -name = "ethbloom" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c22d4b5885b6aa2fe5e8b9329fb8d232bf739e434e6b87347c63bdd00c120f60" -dependencies = [ - "crunchy", - "fixed-hash", - "impl-codec", - "impl-rlp", - "impl-serde", - "scale-info", - "tiny-keccak", -] - -[[package]] -name = "ethereum-types" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02d215cbf040552efcbe99a38372fe80ab9d00268e20012b79fcd0f073edd8ee" -dependencies = [ - "ethbloom", - "fixed-hash", - "impl-codec", - "impl-rlp", - "impl-serde", - "primitive-types", - "scale-info", - "uint", -] - -[[package]] -name = "ethers-core" -version = "2.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d80cc6ad30b14a48ab786523af33b37f28a8623fc06afd55324816ef18fb1f" -dependencies = [ - "arrayvec", - "bytes", - "chrono", - "const-hex", - "elliptic-curve", - "ethabi", - "generic-array", - "k256", - "num_enum", - "open-fastrlp", - "rand 0.8.6", - "rlp", - "serde", - "serde_json", - "strum", - "tempfile", - "thiserror 1.0.69", - "tiny-keccak", - "unicode-xid", -] - -[[package]] -name = "ethers-signers" -version = "2.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "228875491c782ad851773b652dd8ecac62cda8571d3bc32a5853644dd26766c2" -dependencies = [ - "async-trait", - "coins-bip32", - "coins-bip39", - "const-hex", - "elliptic-curve", - "eth-keystore", - "ethers-core", - "rand 0.8.6", - "sha2 0.10.9", - "thiserror 1.0.69", - "tracing", -] - -[[package]] -name = "euclid" -version = "0.20.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb7ef65b3777a325d1eeefefab5b6d4959da54747e33bd6258e789640f307ad" -dependencies = [ - "num-traits", -] - [[package]] name = "euclid" version = "0.22.14" @@ -2546,18 +2213,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "fixed-hash" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" -dependencies = [ - "byteorder", - "rand 0.8.6", - "rustc-hex", - "static_assertions", -] - [[package]] name = "flate2" version = "1.1.9" @@ -2932,16 +2587,6 @@ dependencies = [ "polyval", ] -[[package]] -name = "gif" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" -dependencies = [ - "color_quant", - "weezl", -] - [[package]] name = "gimli" version = "0.32.3" @@ -3248,21 +2893,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hex-conservative" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" -dependencies = [ - "arrayvec", -] - -[[package]] -name = "hex_lit" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" - [[package]] name = "hifijson" version = "0.5.0" @@ -3602,14 +3232,10 @@ checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" dependencies = [ "bytemuck", "byteorder-lite", - "color_quant", - "gif", "moxcms", "num-traits", "png 0.18.1", "tiff", - "zune-core", - "zune-jpeg", ] [[package]] @@ -3627,44 +3253,6 @@ dependencies = [ "nom 7.1.3", ] -[[package]] -name = "impl-codec" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" -dependencies = [ - "parity-scale-codec", -] - -[[package]] -name = "impl-rlp" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28220f89297a075ddc7245cd538076ee98b01f2a9c23a53a4f1105d5a322808" -dependencies = [ - "rlp", -] - -[[package]] -name = "impl-serde" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc88fc67028ae3db0c853baa36269d398d5f45b6982f95549ff5def78c935cd" -dependencies = [ - "serde", -] - -[[package]] -name = "impl-trait-for-tuples" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "indexmap" version = "1.9.3" @@ -3845,7 +3433,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4011,21 +3599,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "konst" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" -dependencies = [ - "konst_macro_rules", -] - -[[package]] -name = "konst_macro_rules" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" - [[package]] name = "kurbo" version = "0.11.3" @@ -4033,7 +3606,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" dependencies = [ "arrayvec", - "euclid 0.22.14", + "euclid", "smallvec", ] @@ -4224,34 +3797,6 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -[[package]] -name = "lopdf" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7184fdea2bc3cd272a1acec4030c321a8f9875e877b3f92a53f2f6033fdc289" -dependencies = [ - "aes", - "bitflags 2.11.1", - "cbc", - "ecb", - "encoding_rs", - "flate2", - "getrandom 0.3.4", - "indexmap 2.14.0", - "itoa", - "log", - "md-5", - "nom 8.0.0", - "nom_locate", - "rand 0.9.4", - "rangemap", - "sha2 0.10.9", - "stringprep", - "thiserror 2.0.18", - "ttf-parser", - "weezl", -] - [[package]] name = "lru-slab" version = "0.1.2" @@ -4343,16 +3888,6 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest 0.10.7", -] - [[package]] name = "memchr" version = "2.8.0" @@ -4599,17 +4134,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "nom_locate" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b577e2d69827c4740cba2b52efaad1c4cc7c73042860b199710b3575c68438d" -dependencies = [ - "bytecount", - "memchr", - "nom 8.0.0", -] - [[package]] name = "notify-rust" version = "4.17.0" @@ -4650,7 +4174,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4714,7 +4238,7 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ - "proc-macro-crate 3.5.0", + "proc-macro-crate 1.3.1", "proc-macro2", "quote", "syn 2.0.117", @@ -5154,31 +4678,6 @@ dependencies = [ "pathdiff", ] -[[package]] -name = "open-fastrlp" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786393f80485445794f6043fd3138854dd109cc6c4bd1a6383db304c9ce9b9ce" -dependencies = [ - "arrayvec", - "auto_impl", - "bytes", - "ethereum-types", - "open-fastrlp-derive", -] - -[[package]] -name = "open-fastrlp-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "003b2be5c6c53c1cfeb0a238b8a1c3915cd410feb684457a36c10038f764bb1c" -dependencies = [ - "bytes", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "openhuman" version = "0.63.7" @@ -5191,7 +4690,6 @@ dependencies = [ "async-trait", "axum", "base64 0.22.1", - "bitcoin", "block2 0.6.2", "bs58", "bytes", @@ -5207,8 +4705,6 @@ dependencies = [ "dotenvy", "ed25519-dalek", "enigo", - "ethers-core", - "ethers-signers", "flate2", "fs2", "futures", @@ -5221,6 +4717,7 @@ dependencies = [ "hostname", "hound", "iana-time-zone", + "k256", "keyring", "lettre", "libc", @@ -5232,8 +4729,6 @@ dependencies = [ "objc2-foundation 0.3.2", "once_cell", "parking_lot", - "pdf-extract", - "ppt-rs", "rand 0.10.1", "rdev", "regex", @@ -5280,7 +4775,7 @@ dependencies = [ "tracing-subscriber", "url", "urlencoding", - "uuid 1.23.1", + "uuid", "wait-timeout", "walkdir", "windows-sys 0.61.2", @@ -5420,34 +4915,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "parity-scale-codec" -version = "3.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" -dependencies = [ - "arrayvec", - "bitvec", - "byte-slice-cast", - "const_format", - "impl-trait-for-tuples", - "parity-scale-codec-derive", - "rustversion", - "serde", -] - -[[package]] -name = "parity-scale-codec-derive" -version = "3.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" -dependencies = [ - "proc-macro-crate 3.5.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "parking" version = "2.2.1" @@ -5477,17 +4944,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "password-hash" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" -dependencies = [ - "base64ct", - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "password-hash" version = "0.5.0" @@ -5505,18 +4961,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" -[[package]] -name = "pbkdf2" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" -dependencies = [ - "digest 0.10.7", - "hmac", - "password-hash 0.4.2", - "sha2 0.10.9", -] - [[package]] name = "pbkdf2" version = "0.12.2" @@ -5527,23 +4971,6 @@ dependencies = [ "hmac", ] -[[package]] -name = "pdf-extract" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28ba1758a3d3f361459645780e09570b573fc3c82637449e9963174c813a98" -dependencies = [ - "adobe-cmap-parser", - "cff-parser", - "encoding_rs", - "euclid 0.20.14", - "log", - "lopdf", - "postscript", - "type1-encoding-parser", - "unicode-normalization", -] - [[package]] name = "percent-encoding" version = "2.3.2" @@ -5764,15 +5191,9 @@ checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "pom" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60f6ce597ecdcc9a098e7fddacb1065093a3d66446fa16c675e7e71d1b5c28e6" + "opaque-debug", + "universal-hash", +] [[package]] name = "portable-atomic" @@ -5789,12 +5210,6 @@ dependencies = [ "portable-atomic", ] -[[package]] -name = "postscript" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78451badbdaebaf17f053fd9152b3ffb33b516104eacb45e7864aaa9c712f306" - [[package]] name = "potential_utf" version = "0.1.5" @@ -5810,18 +5225,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" -[[package]] -name = "ppt-rs" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a7c5e7639749a6ad0c5ea5a31636b88f8a6cefb188c99af0486a93ab50be3cf" -dependencies = [ - "thiserror 1.0.69", - "uuid 1.23.1", - "xml-rs", - "zip 0.6.6", -] - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -5847,20 +5250,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "primitive-types" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" -dependencies = [ - "fixed-hash", - "impl-codec", - "impl-rlp", - "impl-serde", - "scale-info", - "uint", -] - [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -5923,21 +5312,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "proptest" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" -dependencies = [ - "bitflags 2.11.1", - "num-traits", - "rand 0.9.4", - "rand_chacha 0.9.0", - "rand_xorshift", - "regex-syntax", - "unarray", -] - [[package]] name = "prost" version = "0.14.3" @@ -5973,16 +5347,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" -[[package]] -name = "quick-xml" -version = "0.36.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" -dependencies = [ - "encoding_rs", - "memchr", -] - [[package]] name = "quick-xml" version = "0.37.5" @@ -6062,7 +5426,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -6174,21 +5538,6 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" -[[package]] -name = "rand_xorshift" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" -dependencies = [ - "rand_core 0.9.5", -] - -[[package]] -name = "rangemap" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" - [[package]] name = "raw-window-handle" version = "0.6.2" @@ -6492,28 +5841,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "rlp" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" -dependencies = [ - "bytes", - "rlp-derive", - "rustc-hex", -] - -[[package]] -name = "rlp-derive" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33d7b2abe0c340d8797fe2907d3f20d3b5ea5908683618bfe80df7f621f672a" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "roxmltree" version = "0.20.0" @@ -6567,12 +5894,6 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" -[[package]] -name = "rustc-hex" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" - [[package]] name = "rustc_version" version = "0.4.1" @@ -6592,7 +5913,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6650,7 +5971,7 @@ dependencies = [ "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6700,15 +6021,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" -[[package]] -name = "salsa20" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" -dependencies = [ - "cipher", -] - [[package]] name = "same-file" version = "1.0.6" @@ -6718,30 +6030,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scale-info" -version = "2.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346a3b32eba2640d17a9cb5927056b08f3de90f65b72fe09402c2ad07d684d0b" -dependencies = [ - "cfg-if", - "derive_more 1.0.0", - "parity-scale-codec", - "scale-info-derive", -] - -[[package]] -name = "scale-info-derive" -version = "2.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6630024bf739e2179b91fb424b28898baf819414262c5d376677dbff1fe7ebf" -dependencies = [ - "proc-macro-crate 3.5.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "schannel" version = "0.1.29" @@ -6763,7 +6051,7 @@ dependencies = [ "serde", "serde_json", "url", - "uuid 1.23.1", + "uuid", ] [[package]] @@ -6827,18 +6115,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "scrypt" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f9e24d2b632954ded8ab2ef9fea0a0c769ea56ea98bddbafbad22caeeadf45d" -dependencies = [ - "hmac", - "pbkdf2 0.11.0", - "salsa20", - "sha2 0.10.9", -] - [[package]] name = "sec1" version = "0.7.3" @@ -6853,26 +6129,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "secp256k1" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" -dependencies = [ - "bitcoin_hashes", - "rand 0.8.6", - "secp256k1-sys", -] - -[[package]] -name = "secp256k1-sys" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" -dependencies = [ - "cc", -] - [[package]] name = "security-framework" version = "2.11.1" @@ -6917,7 +6173,7 @@ checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" dependencies = [ "bitflags 2.11.1", "cssparser", - "derive_more 2.1.1", + "derive_more", "log", "new_debug_unreachable", "phf 0.13.1", @@ -7045,7 +6301,7 @@ dependencies = [ "thiserror 2.0.18", "time", "url", - "uuid 1.23.1", + "uuid", ] [[package]] @@ -7584,45 +6840,12 @@ dependencies = [ "quote", ] -[[package]] -name = "stringprep" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" -dependencies = [ - "unicode-bidi", - "unicode-normalization", - "unicode-properties", -] - [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.117", -] - [[package]] name = "subtle" version = "2.6.1" @@ -7663,7 +6886,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", - "quote", "unicode-ident", ] @@ -7890,7 +7112,7 @@ dependencies = [ "thiserror 2.0.18", "time", "url", - "uuid 1.23.1", + "uuid", "walkdir", ] @@ -8132,10 +7354,10 @@ dependencies = [ "serde_with", "swift-rs", "thiserror 2.0.18", - "toml 1.1.2+spec-1.1.0", + "toml 0.9.12+spec-1.1.0", "url", "urlpattern", - "uuid 1.23.1", + "uuid", "walkdir", ] @@ -8172,7 +7394,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -8344,12 +7566,18 @@ name = "tinybus" version = "0.1.0" dependencies = [ "async-trait", + "flate2", "serde", "serde_json", + "tar", + "tempfile", "thiserror 2.0.18", "tinybus-macros", "tokio", + "toml 0.8.2", "tracing", + "ureq", + "zip 2.4.2", ] [[package]] @@ -8397,7 +7625,7 @@ dependencies = [ "tracing", "url", "urlencoding", - "uuid 1.23.1", + "uuid", "webpki-roots 1.0.7", ] @@ -8428,7 +7656,7 @@ dependencies = [ "tokio", "toml 0.8.2", "tracing", - "uuid 1.23.1", + "uuid", "walkdir", ] @@ -8443,14 +7671,13 @@ dependencies = [ "serde_json", "sha2 0.10.9", "thiserror 2.0.18", - "uuid 1.23.1", + "uuid", ] [[package]] name = "tinydocs" -version = "0.1.0" +version = "0.1.12" dependencies = [ - "docx-rs", "serde", "thiserror 2.0.18", ] @@ -8512,7 +7739,7 @@ dependencies = [ [[package]] name = "tinymemory" -version = "0.1.0" +version = "0.3.0" dependencies = [ "anyhow", "async-trait", @@ -8535,7 +7762,7 @@ dependencies = [ "serde_json", "sha2 0.11.0", "thiserror 2.0.18", - "uuid 1.23.1", + "uuid", ] [[package]] @@ -8567,7 +7794,7 @@ dependencies = [ "tokio", "tracing", "url", - "uuid 1.23.1", + "uuid", "walkdir", ] @@ -8636,15 +7863,17 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tinywallet" -version = "0.1.0" +version = "0.2.0" dependencies = [ "async-trait", - "bitcoin", + "bech32 0.11.1", "bs58", + "coins-bip32", "coins-bip39", "ed25519-dalek", "hex", "hmac", + "ripemd", "serde", "serde_json", "sha2 0.10.9", @@ -9071,15 +8300,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "type1-encoding-parser" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa10c302f5a53b7ad27fd42a3996e23d096ba39b5b8dd6d9e683a05b01bee749" -dependencies = [ - "pom", -] - [[package]] name = "typed-arena" version = "2.0.2" @@ -9109,18 +8329,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "uint" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" -dependencies = [ - "byteorder", - "crunchy", - "hex", - "static_assertions", -] - [[package]] name = "uname" version = "0.1.1" @@ -9130,12 +8338,6 @@ dependencies = [ "libc", ] -[[package]] -name = "unarray" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" - [[package]] name = "unic-char-property" version = "0.9.0" @@ -9207,15 +8409,6 @@ 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-properties" version = "0.1.4" @@ -9284,6 +8477,35 @@ dependencies = [ "typenum", ] +[[package]] +name = "ureq" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "972d7902c8735f2695410b8aed7df6ed12a47394aa1c8d7af49f0497b731a94d" +dependencies = [ + "base64 0.23.1", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "ureq-proto", + "utf8-zero", + "webpki-roots 1.0.7", +] + +[[package]] +name = "ureq-proto" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da5f78b09e6941e1a0f2e30e695e4b120377b54d5e0aec11b594bb57b3971613" +dependencies = [ + "base64 0.23.1", + "http", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.8" @@ -9348,6 +8570,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -9360,16 +8588,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "uuid" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" -dependencies = [ - "getrandom 0.2.17", - "serde", -] - [[package]] name = "uuid" version = "1.23.1" @@ -9807,7 +9025,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -10653,12 +9871,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" -[[package]] -name = "xml-rs" -version = "0.8.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" - [[package]] name = "xmlwriter" version = "0.1.0" @@ -10725,7 +9937,7 @@ dependencies = [ "tokio", "tracing", "uds_windows", - "uuid 1.23.1", + "uuid", "windows-sys 0.61.2", "winnow 1.0.2", "zbus_macros", @@ -10853,26 +10065,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "zip" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" -dependencies = [ - "aes", - "byteorder", - "bzip2", - "constant_time_eq", - "crc32fast", - "crossbeam-utils", - "flate2", - "hmac", - "pbkdf2 0.11.0", - "sha1", - "time", - "zstd", -] - [[package]] name = "zip" version = "2.4.2" @@ -10920,35 +10112,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "zstd" -version = "0.11.2+zstd.1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "5.0.2+zstd.1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" -dependencies = [ - "libc", - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] - [[package]] name = "zune-core" version = "0.5.1" diff --git a/src/core/runtime/context.rs b/src/core/runtime/context.rs index d563ff7c7a..de27faec16 100644 --- a/src/core/runtime/context.rs +++ b/src/core/runtime/context.rs @@ -514,6 +514,13 @@ pub async fn init_stores( // vectors into the wrong embedding space or make a sync run look empty // instead of broken. crate::openhuman::memory::host_impls::install_memory_host_seams(Arc::new(cfg.clone())); + // Publish the config a module-backed memory driver should load + // against, before the binding below can construct one. Boot-only and + // idempotent (first call wins) — see `modules::memory::set_modules_policy` + // for why this must be a process-global rather than threaded through + // `MemoryBinding::for_workspace`. + #[cfg(feature = "modules")] + crate::openhuman::modules::memory::set_modules_policy(Arc::new(cfg.clone())); match crate::openhuman::memory::global::init(cfg.workspace_dir.clone()) { Ok(_) => log::info!( "[boot] memory::global initialized (workspace={})", diff --git a/src/core/subsystem/driver.rs b/src/core/subsystem/driver.rs index 771beabebb..ae23a8ac34 100644 --- a/src/core/subsystem/driver.rs +++ b/src/core/subsystem/driver.rs @@ -46,6 +46,28 @@ pub enum DriverClass { /// An out-of-process backend reached through a transport adapter over a /// documented wire contract. External, + /// A loadable native module: a `cdylib` admitted through tinybus's ABI, + /// manifest and digest gates and reached over the in-process module bus. + /// + /// Neither of the two classes above fits, and the difference is the policy + /// this class gates: + /// + /// - not [`Self::Embedded`], because the code is **not compiled into this + /// binary**. It is downloaded, verified against a digest pinned in + /// `modules::registry`, and `dlopen`ed. Whether it is present at all is a + /// runtime fact, so a capability set derived from it can be empty on a + /// platform no artifact is published for. + /// - not [`Self::External`], because there is **no egress and no process + /// boundary**. It shares this address space, these privileges and this + /// crash domain, so the trust checks that make sense for a remote backend + /// (endpoint allowlisting, TLS, credential scoping) are neither applicable + /// nor sufficient. What protects the host is admission, not isolation. + /// + /// Treating a module as `External` would apply egress policy to something + /// that makes no network calls while implying an isolation the loader does + /// not provide; treating it as `Embedded` would claim a compile-time + /// guarantee that a downloaded artifact does not have. + Module, /// A stub advertising zero capabilities — what a compiled-out or /// unconfigured subsystem binds to. Null, @@ -53,9 +75,10 @@ pub enum DriverClass { impl DriverClass { /// Every class, in declaration order. - pub const ALL: [DriverClass; 3] = [ + pub const ALL: [DriverClass; 4] = [ DriverClass::Embedded, DriverClass::External, + DriverClass::Module, DriverClass::Null, ]; @@ -69,6 +92,7 @@ impl DriverClass { match self { Self::Embedded => "embedded", Self::External => "external", + Self::Module => "module", Self::Null => "null", } } diff --git a/src/openhuman/memory/binding.rs b/src/openhuman/memory/binding.rs index 8d6ec308d0..41962e0941 100644 --- a/src/openhuman/memory/binding.rs +++ b/src/openhuman/memory/binding.rs @@ -254,7 +254,7 @@ pub(crate) fn reserved_class(id: &str) -> Option { /// The contract's driver class in the kernel's generic vocabulary. /// -/// A total three-arm match, which is why both enums were shaped one-for-one. +/// A total match, which is why both enums were shaped one-for-one. /// The kernel's own enum is deliberately not replaced by the contract's: it is /// shared with the subsystems that come after memory, which must not inherit /// their vocabulary from a *memory* crate. @@ -262,6 +262,7 @@ fn from_contract_class(class: ContractDriverClass) -> DriverClass { match class { ContractDriverClass::Embedded => DriverClass::Embedded, ContractDriverClass::External => DriverClass::External, + ContractDriverClass::Module => DriverClass::Module, ContractDriverClass::Null => DriverClass::Null, } } @@ -307,18 +308,67 @@ pub fn admit(cfg: &MemorySubsystemConfig) -> Result<(String, DriverClass), Fallb fn build(workspace_dir: &Path, cfg: &MemorySubsystemConfig) -> MemoryBinding { match admit(cfg) { Ok((driver_id, class)) => { - let provider: Arc = match class { + // Both the provider *and* the class it should be reported under. The + // two can differ: a build without the `modules` feature admits the + // module class but can only bind a placeholder, and reporting the + // admitted class there would advertise a module-backed driver with a + // null behind it — a live surface with no store, which is exactly + // what status output exists to make visible. + let (provider, class): (Arc, DriverClass) = match class { // Construction is deliberately sync and I/O-free: this runs on // `CoreContext::memory_binding`, which ~4000 pre-boot tests // call with no tokio runtime. The driver resolves its client on // first use — see `driver::embedded`'s module docs. - DriverClass::Embedded => { - Arc::new(EmbeddedMemoryProvider::new(workspace_dir, cfg.hooks)) - } - DriverClass::Null => Arc::new(NullMemoryProvider::new()), + DriverClass::Embedded => ( + Arc::new(EmbeddedMemoryProvider::new(workspace_dir, cfg.hooks)), + DriverClass::Embedded, + ), + DriverClass::Null => (Arc::new(NullMemoryProvider::new()), DriverClass::Null), // Unreachable: `admit` refuses every external driver above, so // this arm cannot bind a transport that does not exist yet. - DriverClass::External => Arc::new(NullMemoryProvider::new()), + // Reported as `Null`, for the same reason as the arm below: what + // bound is a placeholder, and status must say so. + DriverClass::External => (Arc::new(NullMemoryProvider::new()), DriverClass::Null), + // This function builds an `Arc`, while `modules::memory::ModuleMemoryProvider` + // implements `tinymemory_api::provider::MemoryProvider`. Those are + // two different traits from two different crates. + // `TinyMemoryContractAdapter` is the bridge: it wraps the module + // provider and implements the tinycortex-side trait by converting + // each call across the seam (see its module docs for why most + // conversions destructure exhaustively while a few cross by serde + // round trip). It is a temporary bridge — it exists to be deleted + // once the binding itself migrates onto the tinymemory contract, + // at which point `ModuleMemoryProvider` binds directly here. + // + // Gated on the `modules` feature because the concrete provider + // type lives behind it (unlike the adapter above, which is + // generic and feature-independent). A build without `modules` + // cannot load the module driver at all, so it falls back to the + // same placeholder every other inadmissible driver gets, logged + // loudly rather than silently — kernel.md §3.7. + #[cfg(feature = "modules")] + DriverClass::Module => ( + Arc::new( + crate::openhuman::memory::driver::module_adapter::TinyMemoryContractAdapter::new( + Arc::new( + crate::openhuman::modules::memory::ModuleMemoryProvider::from_boot_policy(), + ), + ), + ), + DriverClass::Module, + ), + #[cfg(not(feature = "modules"))] + DriverClass::Module => { + log::warn!( + "[memory:binding] workspace={} configured driver='{}' resolves to the \ + module class, but this build was compiled without the `modules` \ + feature; falling back to the null placeholder", + workspace_dir.display(), + driver_id, + ); + (Arc::new(NullMemoryProvider::new()), DriverClass::Null) + } }; // The configured trust state for the driver that actually bound. // Absent `[subsystems.memory.drivers.]` entry ⇒ the fail-closed diff --git a/src/openhuman/memory/binding_tests.rs b/src/openhuman/memory/binding_tests.rs index 6524cd754f..b0206f0b0a 100644 --- a/src/openhuman/memory/binding_tests.rs +++ b/src/openhuman/memory/binding_tests.rs @@ -636,3 +636,37 @@ fn the_embedded_driver_never_disables_memory() { let binding = for_workspace(dir.path(), &MemorySubsystemConfig::default()).expect("binds"); assert!(!binding.disables_memory()); } + +// A build that admits the module class but cannot construct a module-backed +// provider must not *report* the module class. `bind_provider` receives the +// class that status, `modules.status` and the boot log all read, so passing the +// admitted class while binding a placeholder advertises a live module-backed +// surface with a null store behind it — the one failure this codebase's drift +// guards exist to prevent, and the shape a reviewer caught here. + +#[cfg(not(feature = "modules"))] +#[test] +fn a_module_driver_reports_the_null_class_when_the_feature_is_off() { + let cfg = cfg_with_class("tinymemory", "module"); + let binding = super::build(std::path::Path::new("/tmp/openhuman-binding-test"), &cfg); + assert_eq!( + binding.class(), + crate::core::subsystem::DriverClass::Null, + "a placeholder must not be reported as module-backed" + ); +} + +#[cfg(feature = "modules")] +#[test] +fn a_module_driver_reports_the_module_class_when_the_feature_is_on() { + // The other direction, so the arm above cannot be "fixed" by making every + // module binding report Null. Construction stays I/O-free, so this needs no + // runtime and loads nothing. + let cfg = cfg_with_class("tinymemory", "module"); + let binding = super::build(std::path::Path::new("/tmp/openhuman-binding-test"), &cfg); + assert_eq!( + binding.class(), + crate::core::subsystem::DriverClass::Module, + "a real module binding must report the module class" + ); +} diff --git a/src/openhuman/memory/driver/mod.rs b/src/openhuman/memory/driver/mod.rs index d91ca01a80..ef2f3d59f3 100644 --- a/src/openhuman/memory/driver/mod.rs +++ b/src/openhuman/memory/driver/mod.rs @@ -10,3 +10,4 @@ //! would be meaningless. pub mod embedded; +pub mod module_adapter; diff --git a/src/openhuman/memory/driver/module_adapter.rs b/src/openhuman/memory/driver/module_adapter.rs new file mode 100644 index 0000000000..c31491cdfb --- /dev/null +++ b/src/openhuman/memory/driver/module_adapter.rs @@ -0,0 +1,294 @@ +//! Bridges a TinyMemory-contract driver into the TinyCortex-contract slot. +//! +//! `memory::binding::build` produces an `Arc`, while `modules::memory::ModuleMemoryProvider` implements +//! `tinymemory_api::provider::MemoryProvider`. Those are two distinct traits in +//! two distinct crates. This adapter is what lets the module-backed driver bind +//! before the host's own memory binding finishes migrating onto the TinyMemory +//! contract. +//! +//! # It exists to be deleted +//! +//! `tinymemory-api` was moved *out of* `tinycortex-api`, so the two describe the +//! same values with the same trait shape — `MemoryProvider: MemoryCore + +//! MemoryRecall + MemoryPortability` on both sides, with matching method +//! signatures. When the binding migrates, this file goes away and +//! `ModuleMemoryProvider` is bound directly. +//! +//! # Why serde round-trips and not field-by-field conversion +//! +//! This is the one decision here worth arguing with, so the argument is written +//! down. +//! +//! `tinymemory-tinycortex::convert` does it properly: exhaustive destructuring, +//! total matches, no `..`, so a field added to one contract is a compile error +//! rather than a silently dropped value. That discipline is right for a permanent +//! seam, and it is why the two conversions this adapter needed most +//! (`entry_to_tinycortex`, `namespace_summary_to_tinycortex`) were added there. +//! +//! The remaining types — `ExportPage`, `ExportRecord`, `ImportOutcome`, +//! `SourceScope`, `OwnedRecallOpts` — cross here by serde instead, and that is a +//! deliberate trade with a real downside: a round trip **tolerates** drift where +//! destructuring would catch it. A field added to one side and not the other is +//! dropped at runtime, silently. +//! +//! Three things make that acceptable *for this file specifically*: +//! +//! 1. The contracts are byte-identical today by construction, not by coincidence. +//! 2. [`round_trip_preserves_every_field`] fails if a value stops surviving the +//! crossing, which is the failure mode being traded away — so it is detected, +//! just at test time rather than compile time. +//! 3. This code has a scheduled death. Writing ~250 lines of exhaustive +//! conversion for a seam that is removed once the binding migrates would be +//! work done twice. +//! +//! Do **not** copy this approach into `convert.rs`, and do not extend this file +//! to carry a permanent conversion. If it stops being temporary, replace the +//! round trips with destructuring first. + +use std::sync::Arc; + +use async_trait::async_trait; +use tinycortex_api::capabilities::Capabilities as TcCapabilities; +use tinycortex_api::error::MemoryError as TcError; +use tinycortex_api::health::MemoryHealth as TcHealth; +use tinycortex_api::provider::types::{ + ExportPage as TcExportPage, ExportRecord as TcExportRecord, ImportOutcome as TcImportOutcome, + SourceScope as TcSourceScope, +}; +use tinycortex_api::provider::{ + MemoryCore as TcCore, MemoryPortability as TcPortability, MemoryProvider as TcProvider, + MemoryRecall as TcRecall, +}; +use tinycortex_api::recall::OwnedRecallOpts as TcRecallOpts; +use tinycortex_api::types::{ + MemoryCategory as TcCategory, MemoryEntry as TcEntry, MemoryTaint as TcTaint, + NamespaceSummary as TcSummary, +}; +use tinymemory_api::provider::MemoryProvider as TmProvider; +use tinymemory_tinycortex::convert; + +/// A TinyMemory driver, presented as a TinyCortex one. +pub struct TinyMemoryContractAdapter { + inner: Arc, +} + +impl std::fmt::Debug for TinyMemoryContractAdapter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TinyMemoryContractAdapter") + .field("driver_id", &self.inner.driver_id()) + .finish_non_exhaustive() + } +} + +impl TinyMemoryContractAdapter { + /// Wrap `inner` so it can bind in a TinyCortex-contract slot. + #[must_use] + pub fn new(inner: Arc) -> Self { + Self { inner } + } +} + +/// Cross a value between the two contracts through its serde form. +/// +/// # Errors +/// +/// A message naming the type, when the two contracts have drifted far enough +/// that a value no longer decodes. Never carries the value itself — these are +/// user memory records. +fn cross( + value: &A, + what: &'static str, +) -> Result { + let json = serde_json::to_value(value) + .map_err(|error| TcError::Other(anyhow::anyhow!("encode {what}: {error}")))?; + serde_json::from_value(json) + .map_err(|error| TcError::Other(anyhow::anyhow!("decode {what}: {error}"))) +} + +/// Map a TinyMemory error onto the TinyCortex one, preserving the variant. +/// +/// Variant-preserving rather than collapsing onto `Other`, because a host +/// re-raises these to its own callers and `NotFound` versus `Invalid` is +/// observable — `get`'s contract makes a miss `Ok(None)` while an `Invalid` is a +/// real failure. `Io` and `Serde` degrade to `Other`: neither foreign error can +/// be rebuilt, and inventing an `io::ErrorKind` would be worse than being honest +/// that this crossed a boundary. +fn error_to_tinycortex(error: tinymemory_api::error::MemoryError) -> TcError { + use tinymemory_api::error::MemoryError as Tm; + match error { + Tm::NotFound(message) => TcError::NotFound(message), + Tm::Invalid(message) => TcError::Invalid(message), + Tm::BudgetExceeded(message) => TcError::BudgetExceeded(message), + Tm::PathEscape(message) => TcError::PathEscape(message), + Tm::Unsupported { capability } => TcError::unsupported_raw(capability), + Tm::Io(inner) => TcError::Other(anyhow::anyhow!("io error: {inner}")), + Tm::Serde(inner) => TcError::Other(anyhow::anyhow!("serde error: {inner}")), + Tm::Other(inner) => TcError::Other(inner), + } +} + +#[async_trait] +impl TcProvider for TinyMemoryContractAdapter { + fn driver_id(&self) -> &str { + self.inner.driver_id() + } + + fn capabilities(&self) -> TcCapabilities { + // Both sides serialize a capability set as a JSON array of family names, + // so this crossing is over stable wire strings rather than bit layouts. + // An unreadable set is reported as empty, which is the fail-closed + // direction: the kernel filters its RPC surface from this, so an + // overstated set would register methods that answer errors. + cross::<_, TcCapabilities>(&self.inner.capabilities(), "capabilities") + .unwrap_or_else(|_| TcCapabilities::empty()) + } + + async fn health(&self) -> TcHealth { + let health = self.inner.health().await; + cross::<_, TcHealth>(&health, "health") + .unwrap_or_else(|error| TcHealth::down(error.to_string())) + } + + async fn shutdown(&self) -> Result<(), TcError> { + self.inner.shutdown().await.map_err(error_to_tinycortex) + } +} + +#[async_trait] +impl TcCore for TinyMemoryContractAdapter { + async fn store( + &self, + namespace: &str, + key: &str, + content: &str, + category: TcCategory, + session_id: Option<&str>, + taint: TcTaint, + ) -> Result<(), TcError> { + // Category and taint use the audited destructuring conversions, not a + // serde round trip. Taint especially: mapping it wrongly would let + // externally-sourced content be treated as internal-trust content, which + // is the one thing the policy guard exists to prevent. + self.inner + .store( + namespace, + key, + content, + convert::category_to_tinymemory(category), + session_id, + convert::taint_to_tinymemory(taint), + ) + .await + .map_err(error_to_tinycortex) + } + + async fn get(&self, namespace: &str, key: &str) -> Result, TcError> { + Ok(self + .inner + .get(namespace, key) + .await + .map_err(error_to_tinycortex)? + .map(convert::entry_to_tinycortex)) + } + + async fn forget(&self, namespace: &str, key: &str) -> Result { + self.inner + .forget(namespace, key) + .await + .map_err(error_to_tinycortex) + } + + async fn list( + &self, + namespace: Option<&str>, + category: Option<&TcCategory>, + session_id: Option<&str>, + ) -> Result, TcError> { + let category = category.cloned().map(convert::category_to_tinymemory); + Ok(self + .inner + .list(namespace, category.as_ref(), session_id) + .await + .map_err(error_to_tinycortex)? + .into_iter() + .map(convert::entry_to_tinycortex) + .collect()) + } + + async fn namespaces(&self) -> Result, TcError> { + Ok(self + .inner + .namespaces() + .await + .map_err(error_to_tinycortex)? + .into_iter() + .map(convert::namespace_summary_to_tinycortex) + .collect()) + } +} + +#[async_trait] +impl TcRecall for TinyMemoryContractAdapter { + async fn recall( + &self, + query: &str, + limit: usize, + opts: &TcRecallOpts, + scope: Option<&TcSourceScope>, + ) -> Result, TcError> { + let opts = cross::<_, tinymemory_api::recall::OwnedRecallOpts>(opts, "recall options")?; + // `scope` is a query predicate the driver applies internally, so it has + // to cross intact rather than being applied to the result here — see the + // contract's note on why filtering afterwards is wrong. + let scope = match scope { + Some(scope) => Some(cross::<_, tinymemory_api::provider::types::SourceScope>( + scope, + "source scope", + )?), + None => None, + }; + Ok(self + .inner + .recall(query, limit, &opts, scope.as_ref()) + .await + .map_err(error_to_tinycortex)? + .into_iter() + .map(convert::entry_to_tinycortex) + .collect()) + } +} + +#[async_trait] +impl TcPortability for TinyMemoryContractAdapter { + async fn export_page( + &self, + cursor: Option<&str>, + limit: usize, + ) -> Result { + let page = self + .inner + .export_page(cursor, limit) + .await + .map_err(error_to_tinycortex)?; + cross(&page, "export page") + } + + async fn import_records( + &self, + records: Vec, + ) -> Result { + let records: Vec = + cross(&records, "export records")?; + let outcome = self + .inner + .import_records(records) + .await + .map_err(error_to_tinycortex)?; + cross(&outcome, "import outcome") + } +} + +#[cfg(test)] +#[path = "module_adapter_tests.rs"] +mod tests; diff --git a/src/openhuman/memory/driver/module_adapter_tests.rs b/src/openhuman/memory/driver/module_adapter_tests.rs new file mode 100644 index 0000000000..98a35e55ae --- /dev/null +++ b/src/openhuman/memory/driver/module_adapter_tests.rs @@ -0,0 +1,180 @@ +//! The serde crossings are the risk this adapter takes on, so these tests are +//! about the crossings rather than about the forwarding. + +use super::{cross, error_to_tinycortex}; + +#[test] +fn round_trip_preserves_every_field() { + // This is the test the module docs point at. A serde round trip tolerates + // drift where exhaustive destructuring would catch it, so the trade is only + // acceptable while something checks that values still survive the crossing. + // + // Each case is populated with non-default values in every field it has, so a + // dropped field shows up as an inequality rather than as two defaults that + // happen to match. `..Default::default()` is deliberately not used here: + // a field left on its default would round-trip back to that same default + // even if the crossing dropped it, silently passing. + let opts = tinycortex_api::recall::OwnedRecallOpts { + namespace: Some("work".to_string()), + category: Some(tinycortex_api::types::MemoryCategory::Core), + session_id: Some("session-9".to_string()), + min_score: Some(0.42), + cross_session: true, + }; + let crossed: tinymemory_api::recall::OwnedRecallOpts = + cross(&opts, "recall options").expect("recall options cross"); + let back: tinycortex_api::recall::OwnedRecallOpts = + cross(&crossed, "recall options").expect("recall options cross back"); + assert_eq!(back, opts, "recall options lost a field crossing contracts"); +} + +#[test] +fn an_export_page_survives_the_crossing_with_its_cursor() { + // `next_cursor` is the terminator a caller keys on — an empty `records` is + // explicitly not one — so losing it would turn a paged export into an + // infinite loop or a silent truncation. + let page = tinycortex_api::provider::types::ExportPage { + records: Vec::new(), + next_cursor: Some("cursor-42".to_string()), + }; + let crossed: tinymemory_api::provider::types::ExportPage = + cross(&page, "export page").expect("cross"); + assert_eq!(crossed.next_cursor.as_deref(), Some("cursor-42")); + + let back: tinycortex_api::provider::types::ExportPage = + cross(&crossed, "export page").expect("cross back"); + assert_eq!(back, page); +} + +#[test] +fn an_export_record_survives_the_crossing_with_its_payload_and_taint() { + // `taint` and `payload` are exactly the fields a silent drop would corrupt + // worst: a dropped taint would let externally-sourced content re-enter as + // internal-trust, and a dropped payload would import an empty record. + let record = tinycortex_api::provider::types::ExportRecord { + kind: "entry".to_string(), + id: "entry-7".to_string(), + namespace: Some("work".to_string()), + taint: tinycortex_api::types::MemoryTaint::ExternalSync, + payload: serde_json::json!({"key": "value", "n": 3}), + }; + let crossed: tinymemory_api::provider::types::ExportRecord = + cross(&record, "export record").expect("cross"); + let back: tinycortex_api::provider::types::ExportRecord = + cross(&crossed, "export record").expect("cross back"); + assert_eq!( + back, record, + "export record lost a field crossing contracts" + ); +} + +#[test] +fn an_import_outcome_survives_the_crossing_with_its_counts_and_errors() { + // `errors` is operator-facing diagnosis; a dropped value here would leave + // a non-zero `failed` count with nothing to explain it. + let outcome = tinycortex_api::provider::types::ImportOutcome { + imported: 4, + skipped: 2, + failed: 1, + errors: vec!["record 'x' rejected: bad payload".to_string()], + }; + let crossed: tinymemory_api::provider::types::ImportOutcome = + cross(&outcome, "import outcome").expect("cross"); + let back: tinycortex_api::provider::types::ImportOutcome = + cross(&crossed, "import outcome").expect("cross back"); + assert_eq!( + back, outcome, + "import outcome lost a field crossing contracts" + ); +} + +#[test] +fn a_source_scope_survives_the_crossing_with_every_allowed_source() { + // An empty scope is fail-closed (denies all source-attributed content, per + // the type's own docs), so losing an entry here would silently widen what + // a recall call is allowed to see. + let scope = tinycortex_api::provider::types::SourceScope::new(["src-a", "src-b"]); + let crossed: tinymemory_api::provider::types::SourceScope = + cross(&scope, "source scope").expect("cross"); + let back: tinycortex_api::provider::types::SourceScope = + cross(&crossed, "source scope").expect("cross back"); + assert_eq!(back, scope, "source scope lost an entry crossing contracts"); +} + +#[test] +fn capabilities_cross_as_family_names_not_bit_layouts() { + // Both sides serialize a capability set as a JSON array of family names, so + // the crossing is over stable wire strings. If it went over the bitset an + // added family on one side would silently shift every bit above it. + let capabilities = tinymemory_api::capabilities::Capabilities::mandatory(); + let crossed: tinycortex_api::capabilities::Capabilities = + cross(&capabilities, "capabilities").expect("cross"); + + for mandatory in tinycortex_api::capabilities::Capability::MANDATORY { + assert!( + crossed.contains(mandatory), + "{mandatory:?} was lost crossing contracts" + ); + } +} + +#[test] +fn a_not_found_stays_not_found() { + // A host re-raises these to its own callers, and `get`'s contract makes a + // miss `Ok(None)` while an `Invalid` is a real failure — so collapsing the + // two is observable. + let error = error_to_tinycortex(tinymemory_api::error::MemoryError::NotFound( + "absent".to_string(), + )); + assert!( + matches!(error, tinycortex_api::error::MemoryError::NotFound(_)), + "{error:?}" + ); +} + +#[test] +fn a_path_escape_does_not_become_a_caller_mistake() { + // The security-relevant one: a sandbox escape must not be reclassified as a + // malformed argument. + let error = error_to_tinycortex(tinymemory_api::error::MemoryError::PathEscape( + "symlink leaves workspace".to_string(), + )); + assert!( + matches!(error, tinycortex_api::error::MemoryError::PathEscape(_)), + "{error:?}" + ); +} + +#[test] +fn an_unsupported_capability_keeps_its_family_name() { + let error = error_to_tinycortex(tinymemory_api::error::MemoryError::unsupported_raw( + "vendor_extension", + )); + match error { + tinycortex_api::error::MemoryError::Unsupported { capability } => { + assert_eq!(capability, "vendor_extension"); + } + other => panic!("expected Unsupported, got {other:?}"), + } +} + +#[test] +fn taint_and_category_do_not_go_through_serde() { + // Guards the deliberate exception. Taint decides whether externally-sourced + // content is treated as internal-trust content, so it uses the audited + // destructuring conversion in `tinymemory_tinycortex::convert` rather than a + // round trip that would tolerate an added variant. + use tinymemory_tinycortex::convert; + + for taint in [ + tinycortex_api::types::MemoryTaint::Internal, + tinycortex_api::types::MemoryTaint::ExternalSync, + ] { + let crossed = convert::taint_to_tinymemory(taint); + assert_eq!(convert::taint_to_tinycortex(crossed), taint); + } + + let category = tinycortex_api::types::MemoryCategory::Custom("notes".to_string()); + let crossed = convert::category_to_tinymemory(category.clone()); + assert_eq!(convert::category_to_tinycortex(crossed), category); +} diff --git a/src/openhuman/memory/guard/policy.rs b/src/openhuman/memory/guard/policy.rs index 9bd5268c00..dfe66bbbed 100644 --- a/src/openhuman/memory/guard/policy.rs +++ b/src/openhuman/memory/guard/policy.rs @@ -344,7 +344,20 @@ impl GuardPolicy { /// same time as the class check that selects it. pub fn redact_outbound<'a>(&self, content: &'a str) -> Cow<'a, str> { match self.class { - DriverClass::Embedded | DriverClass::Null => Cow::Borrowed(content), + // `Module` sits with the in-process classes, and the test for this + // grouping is "does the content leave the device", not "is the code + // compiled in". A loaded module is in this address space and makes no + // egress of its own — the memory module's embeddings go back *to* the + // host, which is the whole point of that split — so there is no + // transfer here to disclose or scrub. + // + // Scrubbing would also be actively destructive for the same reason it + // would be for `Embedded`: these are the user's own memory writes, and + // sanitizing them would silently corrupt the data being stored rather + // than protect anything. + DriverClass::Embedded | DriverClass::Module | DriverClass::Null => { + Cow::Borrowed(content) + } DriverClass::External => { Cow::Owned(crate::openhuman::memory::store::safety::sanitize_text(content).value) } @@ -356,7 +369,8 @@ impl GuardPolicy { /// unmodified pass-through for embedded and null drivers. pub fn redact_outbound_json(&self, value: serde_json::Value) -> serde_json::Value { match self.class { - DriverClass::Embedded | DriverClass::Null => value, + // Same grouping and the same reason as `redact_outbound`. + DriverClass::Embedded | DriverClass::Module | DriverClass::Null => value, DriverClass::External => { crate::openhuman::memory::store::safety::sanitize_json(&value).value } diff --git a/src/openhuman/memory/guard/policy_tests.rs b/src/openhuman/memory/guard/policy_tests.rs index 195a187098..e29f8a73f5 100644 --- a/src/openhuman/memory/guard/policy_tests.rs +++ b/src/openhuman/memory/guard/policy_tests.rs @@ -250,6 +250,40 @@ fn guard_does_not_redact_for_a_null_driver() { assert_eq!(policy.redact_outbound(SECRETY), SECRETY); } +/// Pins the `Module` pass-through explicitly. The exhaustiveness check on +/// [`GuardPolicy::redact_outbound`]'s `match` guarantees `Module` is handled +/// *somewhere*, but it does not guarantee it stays in the no-op group with +/// `Embedded` — moving it into the `External` arm would silently start +/// sanitizing (corrupting) module-backed memory writes with no compile error +/// to catch it. This test is what would catch that move. +#[test] +fn guard_does_not_redact_for_a_module_driver() { + let policy = GuardPolicy::new( + "tinymemory", + DriverClass::Module, + MemoryHooksConfig::default(), + TRUSTED, + ); + let out = policy.redact_outbound(SECRETY); + assert_eq!(out, SECRETY, "module traffic must be byte-identical"); + assert!( + matches!(out, std::borrow::Cow::Borrowed(_)), + "and must not even be re-allocated" + ); +} + +#[test] +fn guard_does_not_redact_json_for_a_module_driver() { + let policy = GuardPolicy::new( + "tinymemory", + DriverClass::Module, + MemoryHooksConfig::default(), + TRUSTED, + ); + let value = serde_json::json!({ "token": SECRETY }); + assert_eq!(policy.redact_outbound_json(value.clone()), value); +} + #[test] fn guard_redacts_content_for_an_external_driver() { let out = external_policy(TRUSTED).redact_outbound(SECRETY); diff --git a/src/openhuman/modules/boot.rs b/src/openhuman/modules/boot.rs index e5b9159941..70768646af 100644 --- a/src/openhuman/modules/boot.rs +++ b/src/openhuman/modules/boot.rs @@ -57,6 +57,13 @@ pub async fn load_declared_modules(config: &Config) { if record.load != LoadPolicy::Eager { continue; } + if !should_eager_load(record, config) { + log::debug!( + "[modules] eager module '{}' skipped: the memory driver is not module-backed", + record.id + ); + continue; + } if let Err(reason) = ops::ensure_loaded(config, record.id).await { log::warn!( "[modules] eager module '{}' did not load: {reason}", @@ -66,11 +73,80 @@ pub async fn load_declared_modules(config: &Config) { } } +/// Whether `record` should be loaded eagerly at boot, given `config`. +/// +/// Every `LoadPolicy::Eager` record is eager unconditionally, except +/// TinyMemory: it is eager only when this host's memory subsystem actually +/// selected the module-backed driver. Eager-loading it for every +/// `modules.enabled` host — regardless of which memory driver is bound — +/// would mean a host on the (default) `Embedded` driver pays a startup +/// download and native `dlopen` for a module it never binds, breaking the +/// module driver's opt-in contract. +/// +/// [`crate::openhuman::memory::binding::admit`] is the same pure, +/// side-effect-free check `memory::binding::build` itself uses to decide what +/// actually gets bound, so this can never disagree with the real binding. +fn should_eager_load(record: &super::types::ModuleRecord, config: &Config) -> bool { + if record.id != super::memory::MODULE_ID { + return true; + } + matches!( + crate::openhuman::memory::binding::admit(&config.subsystems.memory), + Ok((_, crate::core::subsystem::DriverClass::Module)) + ) +} + #[cfg(test)] mod tests { - use super::load_declared_modules; + use super::{load_declared_modules, should_eager_load}; use crate::openhuman::config::Config; + #[test] + fn tinymemory_is_not_eager_when_the_memory_driver_is_embedded() { + // The default config binds the embedded driver, so the module-backed + // TinyMemory record must not be treated as eager — otherwise every + // host with `modules.enabled` would pay a boot-time download for a + // driver it never binds. + let config = Config::default(); + let record = super::registry::find(super::super::memory::MODULE_ID) + .expect("tinymemory is a registered module"); + assert!(!should_eager_load(record, &config)); + } + + #[test] + fn tinymemory_is_eager_when_the_memory_driver_is_module_backed() { + let mut config = Config::default(); + config.subsystems.memory.driver = "tinymemory".to_string(); + config.subsystems.memory.drivers.insert( + "tinymemory".to_string(), + tinymemory_api::host::MemoryDriverConfig { + class: Some("module".to_string()), + ..Default::default() + }, + ); + let record = super::registry::find(super::super::memory::MODULE_ID) + .expect("tinymemory is a registered module"); + assert!(should_eager_load(record, &config)); + } + + #[test] + fn other_eager_records_are_unconditional() { + // Non-memory eager records (today, none — but the rule must not + // silently start gating an unrelated module the moment one is added + // and marked `Eager`) are unaffected by the memory driver selection. + let config = Config::default(); + for record in super::registry::ALL { + if record.id == super::super::memory::MODULE_ID { + continue; + } + assert!( + should_eager_load(record, &config), + "record '{}' should be unconditionally eager-eligible", + record.id + ); + } + } + #[tokio::test] async fn boot_is_a_no_op_when_modules_are_disabled() { // Must not start a broker as a side effect of being switched off. diff --git a/src/openhuman/modules/memory.rs b/src/openhuman/modules/memory.rs new file mode 100644 index 0000000000..5bfa168f12 --- /dev/null +++ b/src/openhuman/modules/memory.rs @@ -0,0 +1,388 @@ +//! The host half of `ai.tinyhumans.tinymemory.Memory`. +//! +//! [`ModuleMemoryProvider`] implements `MemoryProvider` by forwarding each method +//! to the loaded module. Because the wire surface mirrors the trait one method +//! for one method, there is no translation layer here — only the bus call, the +//! error mapping, and the two decisions below. +//! +//! # Construction is synchronous and does no I/O +//! +//! `memory::binding::build` is called from `CoreContext::memory_binding`, which +//! roughly four thousand pre-boot tests invoke with no tokio runtime at all. So +//! [`ModuleMemoryProvider::new`] cannot load the module, cannot dial the bus, and +//! cannot await anything. It stores its configuration and resolves on first use, +//! the same contract `driver::embedded` follows. +//! +//! That has one consequence worth stating plainly, because it looks like a +//! shortcut and is not: +//! +//! ## `capabilities()` is answered statically +//! +//! `MemoryProvider::capabilities` is a **synchronous** method, and the module can +//! only answer it over the bus. It therefore cannot be asked here. +//! +//! It does not need to be. The module serves exactly the mandatory three — +//! `tinymemory-tinycortex` advertises Core, Recall and Portability because the +//! optional families need a host's configuration, embedding compute and job +//! queue — and that is a property of the artifact's *source*, fixed at the +//! version the registry pins, not something to discover at runtime. So this +//! returns [`Capabilities::mandatory`], and [`ModuleMemoryProvider::verify`] +//! cross-checks it against the module's own answer on first use and logs loudly +//! on disagreement. +//! +//! Guessing high would be the dangerous direction: the kernel filters its RPC +//! surface and agent-tool assembly from this set, so an overstated capability +//! registers methods that answer errors. Guessing exactly is safe; the +//! cross-check catches a future artifact that widens its scope. +//! +//! # Errors round-trip through the shared table +//! +//! `tinymemory_api::wire` maps a `MemoryError` to a `(name, message)` pair and +//! back, and **both ends use it**. Reimplementing the mapping here is what would +//! let a `PathEscape` arrive as an `Invalid`, silently reclassifying a sandbox +//! escape as a caller mistake. + +use std::sync::Arc; + +use async_trait::async_trait; +use tinymemory_api::capabilities::Capabilities; +use tinymemory_api::error::MemoryError; +use tinymemory_api::health::MemoryHealth; +use tinymemory_api::provider::mandatory::{MemoryCore, MemoryPortability, MemoryRecall}; +use tinymemory_api::provider::types::{ExportPage, ExportRecord, ImportOutcome, SourceScope}; +use tinymemory_api::provider::MemoryProvider; +use tinymemory_api::recall::OwnedRecallOpts; +use tinymemory_api::types::{MemoryCategory, MemoryEntry, MemoryTaint, NamespaceSummary}; +use tinymemory_api::wire; + +use super::{host, ops, registry}; +use crate::openhuman::config::Config; + +/// Registry id of the module these calls go to. +pub const MODULE_ID: &str = "tinymemory"; + +/// The `[modules]` policy this process was booted with. +/// +/// # Why a process-global and not a constructor argument +/// +/// `memory::binding::build` is where a module driver is constructed, and it +/// receives only a workspace dir and a `MemorySubsystemConfig`. What +/// [`ops::ensure_loaded`] needs is `modules.{enabled, allow_download, +/// install_dir}`, which lives on the full `Config` — and threading a whole +/// `Config` down through `MemoryBinding::for_workspace` would widen that +/// function's dependency and change a cache key that ~4000 pre-boot tests hit. +/// +/// So the policy is published once during boot instead. This is the same shape +/// `tinymemory_core::embedding_host` and `api::product` already use, and for the +/// same stated reason: the construction sites sit too deep to thread through. +/// +/// # Unset means disabled, deliberately +/// +/// A pre-boot test, or a host that never called [`set_modules_policy`], gets +/// `None` — and [`policy`] then reports modules disabled rather than assuming +/// permissive defaults. Defaulting `enabled` to `true` here would silently +/// ignore an operator who turned modules off, and would let a unit test reach +/// for a download. +static MODULES_POLICY: std::sync::OnceLock> = std::sync::OnceLock::new(); + +/// Publish the config a module driver should load against. +/// +/// Call once during boot, before any workspace is bound. Later calls are +/// ignored — a driver already resolved against the first value must not have the +/// policy change underneath it. +pub fn set_modules_policy(config: Arc) { + let _ = MODULES_POLICY.set(config); +} + +/// The published policy, if boot supplied one. +fn policy() -> Option<&'static Arc> { + MODULES_POLICY.get() +} + +/// A memory driver served by the loaded `tinymemory` module. +pub struct ModuleMemoryProvider { + /// The id reported by [`MemoryProvider::driver_id`]. + driver_id: String, + /// The config to load against, when the caller had one to give. + /// + /// `None` is the binding-site case: `build` has no `Config`, so the provider + /// falls back to the policy published at boot. Tests pass one explicitly. + config: Option>, + /// Set once the module has answered `Capabilities`, so the cross-check runs + /// once rather than per call. + verified: std::sync::OnceLock<()>, +} + +impl std::fmt::Debug for ModuleMemoryProvider { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // `Config` is not rendered: it carries credentials. + f.debug_struct("ModuleMemoryProvider") + .field("driver_id", &self.driver_id) + .finish_non_exhaustive() + } +} + +impl ModuleMemoryProvider { + /// Bind the module-backed driver. + /// + /// Synchronous and I/O-free by requirement — see the module docs. Nothing is + /// loaded until the first call. + #[must_use] + pub fn new(config: Arc) -> Self { + Self::with_optional_config(Some(config)) + } + + /// Bind against the policy published by [`set_modules_policy`]. + /// + /// This is what `memory::binding::build` uses, because it has no `Config` to + /// hand over. If boot published nothing, every call reports the module + /// unavailable rather than guessing a permissive default. + #[must_use] + pub fn from_boot_policy() -> Self { + Self::with_optional_config(None) + } + + fn with_optional_config(config: Option>) -> Self { + Self { + driver_id: registry::find(MODULE_ID) + .map_or_else(|| MODULE_ID.to_string(), |record| record.id.to_string()), + config, + verified: std::sync::OnceLock::new(), + } + } + + /// Ensure the module is serving, and hand back a proxy for its object. + /// + /// `operation` identifies the forwarded call (e.g. `"store"`, `"recall"`) + /// for the diagnostic below. Never `namespace`, `key`, `content`, or any + /// record value — those are user memory content, not correlation fields. + async fn proxy(&self, operation: &str) -> Result { + log::debug!( + "[modules:memory] driver_id={} operation={operation} resolving module proxy", + self.driver_id, + ); + let config = self.config.as_ref().or_else(|| policy()).ok_or_else(|| { + MemoryError::Other(anyhow::anyhow!( + "the module host policy was never published, so module '{MODULE_ID}' \ + cannot be loaded; call modules::memory::set_modules_policy during boot" + )) + })?; + ops::ensure_loaded(config, MODULE_ID) + .await + .map_err(|message| MemoryError::Other(anyhow::anyhow!(message)))?; + + let record = registry::find(MODULE_ID) + .ok_or_else(|| MemoryError::Other(anyhow::anyhow!("unknown module '{MODULE_ID}'")))?; + let runtime = host::runtime().await.map_err(|error| { + MemoryError::Other(anyhow::anyhow!("the module bus is not running: {error}")) + })?; + let proxy = runtime + .proxy(record.bus_name, record.object_path) + .map_err(|error| MemoryError::Other(anyhow::anyhow!(error.to_string())))?; + + self.verify(&proxy).await; + Ok(proxy) + } + + /// Cross-check the module's advertised capabilities against what this build + /// assumes, once per process. + /// + /// Logged rather than fatal. A module that advertises *more* than the + /// mandatory three is not dangerous — the kernel simply will not use the + /// extra families, because it filtered its surface from the static set — but + /// it does mean the registry pin and the artifact have diverged, which is + /// worth seeing. A module advertising *less* is the real problem, and says so. + async fn verify(&self, proxy: &tinybus::Proxy) { + if self.verified.get().is_some() { + return; + } + match proxy.call::("Capabilities", ()).await { + Ok(actual) => { + let assumed = Capabilities::mandatory(); + if actual != assumed { + log::warn!( + "[modules:memory] the module advertises {actual:?} but this build \ + assumes {assumed:?}; the registry pin and the artifact have diverged" + ); + } + } + Err(error) => { + log::warn!("[modules:memory] could not read module capabilities: {error}"); + } + } + let _ = self.verified.set(()); + } +} + +/// Map a bus failure back onto a [`MemoryError`]. +/// +/// Uses the shared table so the host and the module cannot disagree about what a +/// name means. An unrecognised name becomes `Other`, never `Invalid`. +fn from_bus(error: &tinybus::Error) -> MemoryError { + wire::from_wire(error.wire_name(), &error.to_string()) +} + +#[async_trait] +impl MemoryProvider for ModuleMemoryProvider { + fn driver_id(&self) -> &str { + &self.driver_id + } + + /// The mandatory three. See the module docs on why this is static. + fn capabilities(&self) -> Capabilities { + Capabilities::mandatory() + } + + async fn health(&self) -> MemoryHealth { + // An unreachable module is a *health* answer, not an error: that is the + // question this method exists to answer, and returning `Down` is how + // status output shows an unsupported platform or a refused artifact. + match self.proxy("health").await { + Ok(proxy) => proxy + .call::("Health", ()) + .await + .unwrap_or_else(|error| MemoryHealth::down(error.to_string())), + Err(error) => MemoryHealth::down(error.to_string()), + } + } + + async fn shutdown(&self) -> Result<(), MemoryError> { + // Deliberately does not load the module in order to shut it down: a + // shutdown on a driver that was never used should be a no-op, not a + // download. tinybus never unloads a library anyway, so this releases + // backend resources only. + if self.verified.get().is_none() { + return Ok(()); + } + let proxy = self.proxy("shutdown").await?; + proxy + .call::<()>("Shutdown", ()) + .await + .map_err(|error| from_bus(&error)) + } +} + +#[async_trait] +impl MemoryCore for ModuleMemoryProvider { + async fn store( + &self, + namespace: &str, + key: &str, + content: &str, + category: MemoryCategory, + session_id: Option<&str>, + taint: MemoryTaint, + ) -> Result<(), MemoryError> { + // No log line carries `namespace`, `key` or `content`: all three are user + // memory content. + self.proxy("store") + .await? + .call::<()>( + "Store", + ( + namespace, + key, + content, + category, + session_id.map(str::to_string), + taint, + ), + ) + .await + .map_err(|error| from_bus(&error)) + } + + async fn get(&self, namespace: &str, key: &str) -> Result, MemoryError> { + self.proxy("get") + .await? + .call("Get", (namespace, key)) + .await + .map_err(|error| from_bus(&error)) + } + + async fn forget(&self, namespace: &str, key: &str) -> Result { + self.proxy("forget") + .await? + .call("Forget", (namespace, key)) + .await + .map_err(|error| from_bus(&error)) + } + + async fn list( + &self, + namespace: Option<&str>, + category: Option<&MemoryCategory>, + session_id: Option<&str>, + ) -> Result, MemoryError> { + self.proxy("list") + .await? + .call( + "List", + ( + namespace.map(str::to_string), + category.cloned(), + session_id.map(str::to_string), + ), + ) + .await + .map_err(|error| from_bus(&error)) + } + + async fn namespaces(&self) -> Result, MemoryError> { + self.proxy("namespaces") + .await? + .call("Namespaces", ()) + .await + .map_err(|error| from_bus(&error)) + } +} + +#[async_trait] +impl MemoryRecall for ModuleMemoryProvider { + async fn recall( + &self, + query: &str, + limit: usize, + opts: &OwnedRecallOpts, + scope: Option<&SourceScope>, + ) -> Result, MemoryError> { + // `scope` crosses as a value because the driver must apply it as a query + // predicate internally; narrowing the result here instead would let the + // module spend its `limit` on entries the caller may not see. + self.proxy("recall") + .await? + .call("Recall", (query, limit, opts, scope.cloned())) + .await + .map_err(|error| from_bus(&error)) + } +} + +#[async_trait] +impl MemoryPortability for ModuleMemoryProvider { + async fn export_page( + &self, + cursor: Option<&str>, + limit: usize, + ) -> Result { + self.proxy("export_page") + .await? + .call("ExportPage", (cursor.map(str::to_string), limit)) + .await + .map_err(|error| from_bus(&error)) + } + + async fn import_records( + &self, + records: Vec, + ) -> Result { + self.proxy("import_records") + .await? + .call("ImportRecords", (records,)) + .await + .map_err(|error| from_bus(&error)) + } +} + +#[cfg(test)] +#[path = "memory_tests.rs"] +mod tests; diff --git a/src/openhuman/modules/memory_tests.rs b/src/openhuman/modules/memory_tests.rs new file mode 100644 index 0000000000..1a2b419c70 --- /dev/null +++ b/src/openhuman/modules/memory_tests.rs @@ -0,0 +1,192 @@ +//! Tests for the memory module client. +//! +//! Nothing here loads a module. What is testable without one is what decides a +//! caller's next move: that construction is genuinely I/O-free, that the static +//! capability answer is the one the module actually serves, and that a bus error +//! comes back as the right `MemoryError` variant. The round trips are covered +//! where they can be honest — `tinymemory`'s own loader E2E, against a real +//! broker and a real `dlopen`. + +use std::sync::Arc; + +use tinymemory_api::capabilities::{Capabilities, Capability}; +use tinymemory_api::error::MemoryError; +use tinymemory_api::provider::MemoryProvider; + +use super::{from_bus, ModuleMemoryProvider, MODULE_ID}; +use crate::openhuman::config::Config; +use crate::openhuman::modules::registry; + +fn provider() -> ModuleMemoryProvider { + ModuleMemoryProvider::new(Arc::new(Config::default())) +} + +/// A bus failure carrying `name`. +fn failure(name: &str) -> tinybus::Error { + tinybus::Error::MethodFailed { + name: name.to_string(), + message: "something went wrong".to_string(), + } +} + +#[test] +fn construction_touches_no_io_and_needs_no_runtime() { + // The load-bearing property of this type. `CoreContext::memory_binding` is + // synchronous and roughly 4000 pre-boot tests call it with no tokio runtime, + // so a constructor that loaded the module — or merely dialled the bus — would + // panic across the whole suite rather than in one place. + // + // This test runs outside `#[tokio::test]` on purpose: that is what makes it a + // test of the absence of a runtime requirement. + let provider = provider(); + assert_eq!(provider.driver_id(), MODULE_ID); +} + +#[test] +fn the_advertised_capabilities_are_exactly_the_mandatory_three() { + // Must match what the module serves. Overstating is the dangerous direction: + // the kernel filters its RPC surface and agent-tool list from this set, so an + // extra family registers methods that answer errors. + let capabilities = provider().capabilities(); + assert_eq!(capabilities, Capabilities::mandatory()); + + for mandatory in Capability::MANDATORY { + assert!(capabilities.contains(mandatory), "{mandatory:?} is missing"); + } + assert!( + !capabilities.contains(Capability::Tree), + "an optional family the module cannot serve must not be advertised" + ); +} + +#[test] +fn the_registry_record_matches_the_interface_the_module_serves() { + // A record whose bus name or object path disagrees with the module produces a + // proxy that resolves to nothing, and the failure surfaces as an unhelpful + // transport error rather than a mismatch. + let record = registry::find(MODULE_ID).expect("the memory module is registered"); + assert_eq!(record.bus_name, "ai.tinyhumans.tinymemory.Memory"); + assert_eq!(record.object_path, "/ai/tinyhumans/tinymemory/Memory"); +} + +#[test] +fn the_memory_record_publishes_one_asset_per_supported_host() { + // The release exists now, so the question this test used to ask ("are the + // assets deliberately absent?") is settled. What is worth pinning instead is + // that the set is complete: a record missing a host silently reports + // `Unsupported` there rather than failing loudly, so a platform can lose the + // driver without anything saying so. + // + // The digests themselves are checked structurally by `registry`'s own tests + // (lowercase, 64 hex chars) and semantically by tinybus, which refetches the + // release manifest and refuses on disagreement. Nothing here can verify they + // came from the release rather than a local build — that is a review rule, + // and it is written on the record itself. + let record = registry::find(MODULE_ID).expect("registered"); + assert_eq!( + record.assets.len(), + 11, + "expected one asset per released host, got {:?}", + record.assets.iter().map(|a| a.host_key).collect::>() + ); + for asset in record.assets { + assert!( + asset.archive.contains(record.version), + "{} names version-less or mismatched archive {}", + asset.host_key, + asset.archive + ); + } +} + +#[test] +fn a_not_found_survives_the_round_trip_as_not_found() { + // `get`'s contract makes a missing entry `Ok(None)` and an `Invalid` a real + // failure, so collapsing the two would be observable to a caller. + let error = from_bus(&failure(tinymemory_api::wire::NOT_FOUND)); + assert!(matches!(error, MemoryError::NotFound(_)), "{error:?}"); +} + +#[test] +fn an_invalid_input_is_reported_as_something_the_caller_can_fix() { + let error = from_bus(&failure(tinymemory_api::wire::INVALID)); + assert!(matches!(error, MemoryError::Invalid(_)), "{error:?}"); +} + +#[test] +fn a_path_escape_does_not_arrive_as_a_caller_mistake() { + // The mapping's most security-relevant case: a sandbox escape must not be + // reclassified as a malformed argument. + let error = from_bus(&failure(tinymemory_api::wire::PATH_ESCAPE)); + assert!(matches!(error, MemoryError::PathEscape(_)), "{error:?}"); +} + +#[test] +fn an_unsupported_capability_keeps_its_family_name() { + let error = from_bus(&failure(tinymemory_api::wire::UNSUPPORTED)); + assert!( + matches!(error, MemoryError::Unsupported { .. }), + "{error:?}" + ); +} + +#[test] +fn an_unrecognised_wire_name_is_a_backend_failure_not_an_input_error() { + // A module newer than this build may name an error the table lacks. Telling a + // caller its input was wrong when it was not sends it into a rewrite loop over + // something already correct. + let error = from_bus(&failure("ai.tinyhumans.tinymemory.Error.SomethingNewer")); + assert!(matches!(error, MemoryError::Other(_)), "{error:?}"); +} + +#[test] +fn a_missing_module_is_a_backend_failure_the_caller_cannot_fix() { + let error = from_bus(&failure("ai.tinyhumans.tinybus.Error.ModuleUnavailable")); + assert!(matches!(error, MemoryError::Other(_)), "{error:?}"); +} + +#[test] +fn the_debug_form_never_renders_the_config() { + // `Config` carries credentials and `Debug` output reaches logs. + let rendered = format!("{:?}", provider()); + assert!(rendered.contains("ModuleMemoryProvider"), "{rendered}"); + assert!(!rendered.contains("Config"), "{rendered}"); +} + +#[tokio::test] +async fn a_disabled_host_reports_down_rather_than_erroring() { + // `health` is the one method whose job is to answer "is this reachable", so an + // unreachable module is a `Down` health rather than a failure. Status output + // depends on that distinction. + let mut config = Config::default(); + config.modules.enabled = false; + + let provider = ModuleMemoryProvider::new(Arc::new(config)); + let health = provider.health().await; + assert!( + matches!(health, tinymemory_api::health::MemoryHealth::Down { .. }), + "a disabled module host must report Down, got {health:?}" + ); +} + +#[tokio::test] +async fn a_call_against_a_disabled_host_fails_instead_of_hanging() { + let mut config = Config::default(); + config.modules.enabled = false; + + let provider = ModuleMemoryProvider::new(Arc::new(config)); + let outcome = + tinymemory_api::provider::mandatory::MemoryCore::get(&provider, "ns", "key").await; + assert!(outcome.is_err(), "expected an error, got {outcome:?}"); +} + +#[tokio::test] +async fn shutdown_on_an_unused_driver_is_a_no_op() { + // A shutdown must not be the thing that downloads and loads a module. Nothing + // has been used here, so there is nothing to release. + let mut config = Config::default(); + config.modules.enabled = false; + + let provider = ModuleMemoryProvider::new(Arc::new(config)); + assert!(provider.shutdown().await.is_ok()); +} diff --git a/src/openhuman/modules/mod.rs b/src/openhuman/modules/mod.rs index 53b5a4a314..e848fa13f9 100644 --- a/src/openhuman/modules/mod.rs +++ b/src/openhuman/modules/mod.rs @@ -40,6 +40,7 @@ pub mod boot; #[cfg(feature = "documents")] pub mod documents; pub mod host; +pub mod memory; pub mod ops; pub mod platform; pub mod registry; diff --git a/src/openhuman/modules/registry.rs b/src/openhuman/modules/registry.rs index fd755015fe..4bdb9131d4 100644 --- a/src/openhuman/modules/registry.rs +++ b/src/openhuman/modules/registry.rs @@ -173,8 +173,96 @@ const TINYWALLET: ModuleRecord = ModuleRecord { load: LoadPolicy::Lazy, }; +/// The memory engine, served as a driver. +/// +/// # The digests below came from the release, not from a local build +/// +/// Every `sha256` here was copied verbatim from +/// `v0.3.0`'s `checksum.toml`. Recomputing one from a local +/// build would pin whatever this machine happened to produce, which defeats the +/// point: tinybus fetches the release's own manifest, compares it with this +/// value, hashes the download, and only then extracts. Pinning here is the half +/// of that check which is auditable offline, and the half that makes a release +/// re-cut under the same tag stop matching rather than silently replacing what +/// runs in-process. +/// +/// If these ever need to change, take the new values from the release. Do not +/// run `sha256sum` on `target/release/`. +const TINYMEMORY: ModuleRecord = ModuleRecord { + id: "tinymemory", + description: "Local memory engine: store, ranked recall, and portable export", + bus_name: "ai.tinyhumans.tinymemory.Memory", + object_path: "/ai/tinyhumans/tinymemory/Memory", + version: "0.3.0", + release_url: "https://github.com/tinyhumansai/tinymemory/releases/tag/v0.3.0", + assets: &[ + PlatformAsset { + host_key: "ubuntu-24.04-x86_64", + archive: "tinymemory-module-0.3.0-ubuntu-24.04-x86_64.tar.gz", + sha256: "c0406030c7cc09b386bc6040ab29dbafe6dd4f428db9623819c567b4c71afb5a", + }, + PlatformAsset { + host_key: "ubuntu-24.04-arm64", + archive: "tinymemory-module-0.3.0-ubuntu-24.04-arm64.tar.gz", + sha256: "c4e165b68887874acba691dd6a103feea8d0acb401a1d8d00c073bc27404d08f", + }, + PlatformAsset { + host_key: "ubuntu-22.04-x86_64", + archive: "tinymemory-module-0.3.0-ubuntu-22.04-x86_64.tar.gz", + sha256: "e3ac3eb98997bd25b02bdb71c3d565b2e42a89fa4b7f6369758b3ec6a684c857", + }, + PlatformAsset { + host_key: "ubuntu-22.04-arm64", + archive: "tinymemory-module-0.3.0-ubuntu-22.04-arm64.tar.gz", + sha256: "bf06c9540bed6f2c1b95c57f324fd6a33a33bc10a2a7deaddd36fbafc7d02c40", + }, + PlatformAsset { + host_key: "macos-26-arm64", + archive: "tinymemory-module-0.3.0-macos-26-arm64.tar.gz", + sha256: "2d4266d1430c7fa9ec3e609785d534a0fe84a4a1d00ae9fbb151c82c32eccf19", + }, + PlatformAsset { + host_key: "macos-26-x86_64", + archive: "tinymemory-module-0.3.0-macos-26-x86_64.tar.gz", + sha256: "11b63f0492a9c365ebecfc2e7e433cd133b58e0e727322868b6ef3e6edbd8b4b", + }, + PlatformAsset { + host_key: "macos-15-arm64", + archive: "tinymemory-module-0.3.0-macos-15-arm64.tar.gz", + sha256: "7a39d272aa29148ef652727bbc05d67c361c4d706a7d89a5dedab192bbb75642", + }, + PlatformAsset { + host_key: "macos-15-x86_64", + archive: "tinymemory-module-0.3.0-macos-15-x86_64.tar.gz", + sha256: "0c7eba4e64009eec8e1ae19bfc9b5859136ebf6bc6cb7efa7510a61da42f4456", + }, + PlatformAsset { + host_key: "windows-2025-x86_64", + archive: "tinymemory-module-0.3.0-windows-2025-x86_64.zip", + sha256: "4e55458bc79e1c504d1224afb70edd8404fe8ee3bf0f5e2d44bb258cae79fe05", + }, + PlatformAsset { + host_key: "windows-2022-x86_64", + archive: "tinymemory-module-0.3.0-windows-2022-x86_64.zip", + sha256: "608354bf847ee32f90fa6f76c918c20441be9ec09544338867512dbf49b4685d", + }, + PlatformAsset { + host_key: "windows-11-arm64", + archive: "tinymemory-module-0.3.0-windows-11-arm64.zip", + sha256: "b7b55d5a473d43053e791eb94116e9e88fc3533b8a7325c45dad832018f02766", + }, + ], + // Eager, unlike the two codecs above. A codec that is never asked for should + // not be paid for, but a memory driver's absence changes what the kernel + // offers rather than merely delaying it: capabilities are read at bind time + // and the RPC surface and agent-tool list are filtered from them. Resolving + // that during a user's first recall would mean the first recall is the one + // that behaves differently. + load: LoadPolicy::Eager, +}; + /// Every module this build can load. -pub const ALL: &[ModuleRecord] = &[TINYDOCS, TINYWALLET]; +pub const ALL: &[ModuleRecord] = &[TINYDOCS, TINYWALLET, TINYMEMORY]; /// The record for `id`, if this build knows it. #[must_use] @@ -329,6 +417,25 @@ mod tests { ("windows", "aarch64", None), ]; for record in ALL { + // A record with no assets at all is a module whose release has not + // been cut yet. It is exempt from the per-host check — there is + // nothing to be missing — but only if it is on this list, so an + // *accidentally* asset-less record still fails here rather than + // silently becoming unsupported on every platform. + // + // Digests must be copied verbatim from the release `checksum.toml`, + // so shipping placeholder assets to satisfy this test would be worse + // than the exemption. + if record.assets.is_empty() { + assert!( + PENDING_RELEASE.contains(&record.id), + "{} publishes no assets and is not a known pre-release module; \ + either add its assets (digests copied from the release \ + checksum.toml) or add it to PENDING_RELEASE with a reason", + record.id + ); + continue; + } for (os, arch, glibc) in hosts { for key in candidates_for(os, arch, glibc) { assert!( @@ -341,6 +448,19 @@ mod tests { } } + /// Modules registered before their first release exists. + /// + /// Being here means `modules.status` reports `Unsupported` on every platform + /// and `ensure_loaded` refuses, which is the correct behaviour for an artifact + /// that cannot be verified because it has not been published. Removing an + /// entry is the last step of a module port. + /// + /// Empty today: every registered module has a release. Kept rather than + /// deleted because the exemption it grants is what lets the check above stay + /// strict — an accidentally asset-less record must fail, and it can only be + /// told apart from a deliberate one by a list like this. + const PENDING_RELEASE: &[&str] = &[]; + #[test] fn find_resolves_known_ids_only() { assert!(find("tinydocs").is_some()); diff --git a/vendor/tinymemory b/vendor/tinymemory index fd682bd558..82c2a210e8 160000 --- a/vendor/tinymemory +++ b/vendor/tinymemory @@ -1 +1 @@ -Subproject commit fd682bd5583ebf2b4016547cdcbb3360ba726561 +Subproject commit 82c2a210e8f537f8328a4d3022a87baf6aa20f4e