diff --git a/hives/bp7/Cargo.lock b/hives/bp7/Cargo.lock index 9013779e..ff9e7121 100644 --- a/hives/bp7/Cargo.lock +++ b/hives/bp7/Cargo.lock @@ -2,6 +2,41 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.8.12" @@ -31,18 +66,76 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "arc-swap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380962b3d93b99642e50b9febf72c071e97e8b32d86eae9cf4ef582bb960ae6e" +dependencies = [ + "rustversion", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "async_io_stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" +dependencies = [ + "futures", + "pharos", + "rustc_version", +] + [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "attohttpc" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" +dependencies = [ + "base64 0.22.1", + "http", + "log", + "url", +] + [[package]] name = "autocfg" version = "1.5.1" @@ -73,10 +166,76 @@ dependencies = [ ] [[package]] -name = "base-x" -version = "0.2.11" +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + +[[package]] +name = "base16ct" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" [[package]] name = "base64" @@ -117,6 +276,19 @@ version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" +[[package]] +name = "blake3" +version = "1.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" +dependencies = [ + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.1", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -126,6 +298,24 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "borrow-or-share" version = "0.2.4" @@ -134,21 +324,23 @@ checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" [[package]] name = "bp7" -version = "0.10.7" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0dd4a4f935d4040f93aa8b0ccda0ce210911631673ee62a35efba619954b937" +checksum = "03b6bdb8517068b2b959f13b5a0bdb058713402421675987f0525e04a892409b" dependencies = [ "bitflags", "crc", "humantime", - "instant", "nanorand", "serde", + "serde-wasm-bindgen", "serde_bytes", "serde_cbor", "serde_json", - "stdweb", "thiserror 2.0.20", + "wasm-bindgen", + "web-sys", + "web-time", ] [[package]] @@ -156,12 +348,15 @@ name = "bp7" version = "0.33.1" dependencies = [ "anyhow", - "bp7 0.10.7", + "bp7 0.12.1", "hum-identity", "hum-paths", "hum-thrum", + "humd", "serde", "serde_json", + "sha2 0.10.9", + "tempfile", "thrum-core", "tokio", "tracing", @@ -198,12 +393,57 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", +] + [[package]] name = "cmake" version = "0.1.58" @@ -213,6 +453,21 @@ dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.20", +] + [[package]] name = "codegen" version = "0.33.1" @@ -232,12 +487,55 @@ dependencies = [ "memchr", ] +[[package]] +name = "command-group" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68fa787550392a9d58f44c21a3022cfb3ea3e2458b7f85d3b399d0ceeccf409" +dependencies = [ + "async-trait", + "nix", + "tokio", + "winapi", +] + [[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cordyceps" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9ab7e0ca1d179628fa0172b2b97203c7fa0cd81be2448bd446fb9559ca9261" +dependencies = [ + "loom", + "tracing", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -263,6 +561,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.4.0" @@ -278,6 +585,21 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + [[package]] name = "crypto-common" version = "0.1.7" @@ -288,6 +610,34 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", + "rand_core 0.10.1", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "ctutils" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d28fb6aa7dd680b3ffd1a3e64e115c177bdba63c0ae7f9cd48129ff05c4fa0" +dependencies = [ + "cmov", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -295,11 +645,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto 0.2.9", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", "curve25519-dalek-derive", - "digest", - "fiat-crypto", - "rustc_version 0.4.1", + "digest 0.11.3", + "fiat-crypto 0.3.0", + "rand_core 0.10.1", + "rustc_version", + "serde", "subtle", "zeroize", ] @@ -315,6 +683,19 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "data-encoding" version = "2.11.1" @@ -322,31 +703,113 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" [[package]] -name = "der" -version = "0.7.10" +name = "data-encoding-macro" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +checksum = "c6a127ecbb3c4632e1525380e04c0c3fcf8dcb44d32a79ea290d8a36906edcd8" dependencies = [ - "const-oid", - "zeroize", + "data-encoding", + "data-encoding-macro-internal", ] [[package]] -name = "digest" -version = "0.10.7" +name = "data-encoding-macro-internal" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "c54e03a951783e8b327515db3f2a2fd0e3bed362a96b066f341ce66ed49b4ead" dependencies = [ - "block-buffer", - "crypto-common", + "data-encoding", + "syn 3.0.6", ] [[package]] -name = "discard" -version = "1.0.4" +name = "der" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" - +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "zeroize", +] + +[[package]] +name = "der" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a" +dependencies = [ + "const-oid 0.10.2", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", + "unicode-xid", +] + +[[package]] +name = "diatomic-waker" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.7" @@ -358,6 +821,17 @@ dependencies = [ "syn 3.0.6", ] +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "libc", + "once_cell", + "winapi", +] + [[package]] name = "dunce" version = "1.0.5" @@ -370,8 +844,19 @@ version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "pkcs8", - "signature", + "pkcs8 0.10.2", + "signature 2.2.0", +] + +[[package]] +name = "ed25519" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" +dependencies = [ + "pkcs8 0.11.0", + "serdect", + "signature 3.0.0", ] [[package]] @@ -380,11 +865,27 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ - "curve25519-dalek", - "ed25519", - "rand_core", + "curve25519-dalek 4.1.3", + "ed25519 2.2.3", + "rand_core 0.6.4", + "serde", + "sha2 0.10.9", + "subtle", + "zeroize", +] + +[[package]] +name = "ed25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" +dependencies = [ + "curve25519-dalek 5.0.0", + "ed25519 3.0.0", + "rand_core 0.10.1", "serde", - "sha2", + "sha2 0.11.1", + "signature 3.0.0", "subtle", "zeroize", ] @@ -398,6 +899,53 @@ dependencies = [ "serde", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "ensemble" +version = "0.33.1" +dependencies = [ + "anyhow", + "async-trait", + "ed25519-dalek 2.2.0", + "hex", + "hum-identity", + "iroh", + "lru 0.12.5", + "parking_lot", + "rand 0.8.8", + "rustls", + "serde", + "serde_json", + "sha2 0.10.9", + "thiserror 1.0.69", + "thrum-core", + "tokio", + "tokio-rustls", + "tracing", +] + +[[package]] +name = "enum-assoc" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0590c4a94da3372e83493b956755a6e2266830b6e4e3b101afe66e3f39477b91" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -425,12 +973,24 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + [[package]] name = "fiat-crypto" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + [[package]] name = "find-msvc-tools" version = "0.1.13" @@ -454,6 +1014,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foldhash" version = "0.2.0" @@ -485,6 +1051,34 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-buffered" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4421cb78ee172b6b06080093479d3c50f058e7c81b7d577bbb8d118d551d4cd5" +dependencies = [ + "cordyceps", + "diatomic-waker", + "futures-core", + "pin-project-lite", + "spin", +] + [[package]] name = "futures-channel" version = "0.3.34" @@ -501,12 +1095,47 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "futures-sink" version = "0.3.34" @@ -519,14 +1148,22 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" +[[package]] +name = "futures-timer" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" + [[package]] name = "futures-util" version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ + "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -534,6 +1171,21 @@ dependencies = [ "slab", ] +[[package]] +name = "generator" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1be81b31c9ce4caf7f3178eeb90662bb1864d3ad363ec6bdada3201adff19c37" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result 0.4.1", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -578,8 +1230,53 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "governor" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a7f542ee6b35af73b06abc0dad1c1bae89964e4e253bc4b587b91c9637867b" +dependencies = [ + "cfg-if", + "dashmap", + "futures", + "futures-timer", + "no-std-compat", + "nonzero_ext", + "parking_lot", + "portable-atomic", + "quanta", + "rand 0.8.8", + "smallvec", + "spinning_top", ] [[package]] @@ -607,6 +1304,26 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -615,7 +1332,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.2.0", ] [[package]] @@ -623,6 +1340,9 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash 0.2.0", +] [[package]] name = "heck" @@ -630,6 +1350,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + [[package]] name = "hex" version = "0.4.3" @@ -678,22 +1404,54 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hum-identity" version = "0.33.1" dependencies = [ "anyhow", - "ed25519-dalek", + "ed25519-dalek 2.2.0", "hex", "hum-paths", - "rand", + "rand 0.8.8", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tracing", "uuid", ] +[[package]] +name = "hum-nest" +version = "0.33.1" +dependencies = [ + "anyhow", + "async-trait", + "axum", + "command-group", + "hum-identity", + "hum-paths", + "hum-thrum", + "libc", + "lru 0.12.5", + "metrics", + "parking_lot", + "rand 0.8.8", + "regex", + "serde", + "serde_json", + "sysinfo", + "thrum-core", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hum-paths" version = "0.33.1" @@ -711,7 +1469,7 @@ dependencies = [ "anyhow", "hum-identity", "hum-paths", - "rand", + "rand 0.8.8", "serde", "serde_json", "thrum-core", @@ -726,10 +1484,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" [[package]] -name = "hyper" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +name = "humd" +version = "0.33.1" +dependencies = [ + "anyhow", + "async-trait", + "chrono", + "ed25519-dalek 2.2.0", + "ensemble", + "governor", + "hex", + "hum-identity", + "hum-nest", + "hum-paths", + "metrics", + "metrics-exporter-prometheus", + "parking_lot", + "rand 0.8.8", + "serde", + "serde_json", + "thehum", + "thrum-core", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" dependencies = [ "atomic-waker", "bytes", @@ -739,6 +1533,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -784,6 +1579,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.3.0" @@ -867,6 +1686,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "identity-hash" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfdd7caa900436d8f13b2346fe10257e0c05c1f1f9e351f4f5d57c03bd5f45da" + [[package]] name = "idna" version = "1.1.0" @@ -888,6 +1713,26 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "igd-next" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de7238d487a9aff61f81b5ab41c0a841532a115a398b5fa92a2fadd0885e2581" +dependencies = [ + "attohttpc", + "bytes", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "rand 0.10.3", + "tokio", + "url", + "xmltree", +] + [[package]] name = "indexmap" version = "2.14.2" @@ -899,13 +1744,25 @@ dependencies = [ ] [[package]] -name = "instant" -version = "0.1.13" +name = "inout" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ - "cfg-if", - "stdweb", + "generic-array", +] + +[[package]] +name = "ipconfig" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" +dependencies = [ + "socket2", + "widestring", + "windows-registry", + "windows-result 0.4.1", + "windows-sys 0.61.2", ] [[package]] @@ -914,12 +1771,192 @@ version = "2.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" +[[package]] +name = "iroh" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3689cab508af9bcef7816c1201b403595ec6aa20e7d15e1f9683bb4be4a5b17" +dependencies = [ + "backon", + "blake3", + "bytes", + "cfg_aliases", + "ctutils", + "data-encoding", + "derive_more", + "ed25519-dalek 3.0.0", + "futures-util", + "getrandom 0.4.3", + "http", + "ipnet", + "iroh-base", + "iroh-dns", + "iroh-metrics", + "iroh-relay", + "n0-error", + "n0-future", + "n0-watcher", + "netwatch", + "noq", + "noq-proto", + "noq-udp", + "papaya", + "pin-project", + "portable-atomic", + "portmapper", + "rand 0.10.3", + "reqwest", + "rustc-hash", + "rustls", + "rustls-pki-types", + "serde", + "smallvec", + "strum 0.28.0", + "time", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "url", + "wasm-bindgen-futures", +] + +[[package]] +name = "iroh-base" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b72366b16001af3a51574233df88ca6d62da0215807549a769e7de887da803" +dependencies = [ + "curve25519-dalek 5.0.0", + "data-encoding", + "data-encoding-macro", + "derive_more", + "ed25519-dalek 3.0.0", + "getrandom 0.4.3", + "n0-error", + "rand 0.10.3", + "serde", + "url", + "zeroize", +] + +[[package]] +name = "iroh-dns" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7481d0792dde1035a2b06ec06ed3cf6d01fcb1a494e6931b489e32073382048d" +dependencies = [ + "arc-swap", + "cfg_aliases", + "derive_more", + "iroh-base", + "n0-dns-resolver", + "n0-error", + "n0-future", + "portable-atomic", + "rand 0.10.3", + "rustls", + "simple-dns", + "strum 0.28.0", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "iroh-metrics" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ede55536349842337f7cdc63012ec33dc637945f3259c98a0763aac364cdf09" +dependencies = [ + "iroh-metrics-derive", + "itoa", + "n0-error", + "portable-atomic", + "ryu", + "serde", + "tracing", +] + +[[package]] +name = "iroh-metrics-derive" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae5f0c4405d1fbc9fb16ff422ca40620e93dc36c30ecaba0c2aee3992b7bd48" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "iroh-relay" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e5589e7c1475700686603a970275687f760df853bc6525c7eb1347aa31d7230" +dependencies = [ + "blake3", + "bytes", + "cfg_aliases", + "data-encoding", + "derive_more", + "getrandom 0.4.3", + "http", + "http-body-util", + "hyper", + "hyper-util", + "iroh-base", + "iroh-dns", + "iroh-metrics", + "lru 0.18.5", + "n0-error", + "n0-future", + "noq", + "noq-proto", + "num_enum", + "percent-encoding", + "pin-project", + "postcard", + "rand 0.10.3", + "reqwest", + "rustls", + "rustls-pki-types", + "serde", + "serde_bytes", + "strum 0.28.0", + "tokio", + "tokio-rustls", + "tokio-util", + "tokio-websockets", + "tracing", + "url", + "webpki-roots", + "ws_stream_wasm", +] + [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + [[package]] name = "jni" version = "0.22.4" @@ -929,7 +1966,7 @@ dependencies = [ "cfg-if", "combine", "jni-macros", - "jni-sys", + "jni-sys 0.4.1", "log", "simd_cesu8", "thiserror 2.0.20", @@ -945,11 +1982,20 @@ checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" dependencies = [ "proc-macro2", "quote", - "rustc_version 0.4.1", + "rustc_version", "simd_cesu8", "syn 2.0.119", ] +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + [[package]] name = "jni-sys" version = "0.4.1" @@ -1040,6 +2086,12 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.3" @@ -1061,6 +2113,49 @@ version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "lru" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef9ac18847474e638e3702b76c65d4eb93428471a74778ef0f1be711717f89b5" +dependencies = [ + "hashbrown 0.17.1", +] + +[[package]] +name = "lru-slab" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" + +[[package]] +name = "mac-addr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d25b0e0b648a86960ac23b7ad4abb9717601dec6f66c165f5b037f3f03065f" + [[package]] name = "matchers" version = "0.2.0" @@ -1070,6 +2165,12 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "memchr" version = "2.8.3" @@ -1077,29 +2178,427 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] -name = "micromap" -version = "0.3.0" +name = "metrics" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3045b4193fbdc5b5681f32f11070da9be3609f189a79f3390706d42587f46bb5" +dependencies = [ + "ahash", + "portable-atomic", +] + +[[package]] +name = "metrics-exporter-prometheus" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4f0c8427b39666bf970460908b213ec09b3b350f20c0c2eabcbba51704a08e6" +dependencies = [ + "base64 0.22.1", + "http-body-util", + "hyper", + "hyper-util", + "indexmap", + "ipnet", + "metrics", + "metrics-util", + "quanta", + "thiserror 1.0.69", + "tokio", + "tracing", +] + +[[package]] +name = "metrics-util" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4259040465c955f9f2f1a4a8a16dc46726169bca0f88e8fb2dbeced487c3e828" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", + "hashbrown 0.14.5", + "metrics", + "num_cpus", + "quanta", + "sketches-ddsketch", +] + +[[package]] +name = "micromap" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a86d3146ed3995b5913c414f6664344b9617457320782e64f0bb44afd49d74" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "n0-dns-resolver" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535c99c9a786bee714155d2ee4fc2477405c2c43536e1bc6a51de09a0d1448f7" +dependencies = [ + "derive_more", + "ipconfig", + "jni 0.22.4", + "lru 0.18.5", + "n0-error", + "n0-future", + "ndk-context", + "rand 0.10.3", + "reqwest", + "rustc-hash", + "rustls", + "simple-dns", + "system-configuration", + "tokio", + "tokio-rustls", + "tracing", + "webpki-roots", +] + +[[package]] +name = "n0-error" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c59ea174675ce6bc196878851e5049e11b8b1f9af3ba87e4d6df8d828bb001" +dependencies = [ + "n0-error-macros", + "spez", +] + +[[package]] +name = "n0-error-macros" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4412346410d6616f3668c40e53c298e5320c7b856f7a960e5914e442601be79f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "n0-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2ab99dfb861450e68853d34ae665243a88b8c493d01ba957321a1e9b2312bbe" +dependencies = [ + "cfg_aliases", + "derive_more", + "futures-buffered", + "futures-lite", + "futures-util", + "js-sys", + "pin-project", + "send_wrapper", + "tokio", + "tokio-util", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-time", +] + +[[package]] +name = "n0-watcher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64bf1ecb076a87e0e3f5edcf726e7d29e788c273bb0161ba30666b53f085906c" +dependencies = [ + "derive_more", + "n0-error", + "n0-future", + "slotmap", +] + +[[package]] +name = "nanorand" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e3d189da485332e96ba8a5ef646a311871abd7915bf06ac848a9117f19cf6e4" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "netdev" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "096c44b66a9b09b99b4dd36b1c295ff3a492039ccecaf55466bd6ddcdba59968" +dependencies = [ + "block2", + "dispatch2", + "dlopen2", + "ipnet", + "jni 0.21.1", + "libc", + "mac-addr", + "ndk-context", + "netlink-packet-core 0.8.2", + "netlink-packet-route 0.31.0", + "netlink-sys 0.8.8", + "objc2", + "objc2-core-foundation", + "objc2-core-wlan", + "objc2-foundation", + "objc2-system-configuration", + "once_cell", + "plist", + "windows-sys 0.61.2", +] + +[[package]] +name = "netdev" +version = "0.46.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c52c2584961c68f5a6e3356797344061f818c93b8acc9cd6d7e284795e563e" +dependencies = [ + "block2", + "dispatch2", + "dlopen2", + "ipnet", + "jni 0.21.1", + "libc", + "mac-addr", + "ndk-context", + "netlink-packet-core 0.9.0", + "netlink-packet-route 0.33.0", + "netlink-sys 0.9.0", + "objc2-core-foundation", + "objc2-system-configuration", + "once_cell", + "plist", + "windows-sys 0.61.2", +] + +[[package]] +name = "netlink-packet-core" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b897d7bd4f0af82e68d40d0344cf37e97f9c97ddf74a098de3e4da05e96ca395" +dependencies = [ + "paste", +] + +[[package]] +name = "netlink-packet-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6e2daf9a8c2e21302714706860a0e6be02e03d17294ecc66b354ea7d4059dd" + +[[package]] +name = "netlink-packet-route" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2288fcb784eb3defd5fb16f4c4160d5f477de192eac730f43e1d11c24d9a007" +dependencies = [ + "bitflags", + "libc", + "log", + "netlink-packet-core 0.8.2", +] + +[[package]] +name = "netlink-packet-route" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59d48ffc8b73a506e1ff0878528c72668f2bfbc3d875b6be890a96be5d0d5576" +dependencies = [ + "bitflags", + "libc", + "log", + "netlink-packet-core 0.9.0", + "zerocopy", +] + +[[package]] +name = "netlink-proto" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93af8261786086024cd5e96e0a991dd65ced07bbf7c233a487bbc96b971d5539" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "log", + "netlink-packet-core 0.8.2", + "netlink-sys 0.8.8", + "thiserror 2.0.20", +] + +[[package]] +name = "netlink-sys" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" +dependencies = [ + "bytes", + "futures-util", + "libc", + "log", + "tokio", +] + +[[package]] +name = "netlink-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c99d38e00b420df49e940fe0826e667c3dad82ac68eb4c2bbf754c1c14a83a10" +dependencies = [ + "bytes", + "libc", + "log", +] + +[[package]] +name = "netwatch" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39da9cad5f23aa43401f09497d3b280e51b5feba115d9ffbf38ca35d6ff95e96" +dependencies = [ + "atomic-waker", + "bytes", + "cfg_aliases", + "derive_more", + "ipnet", + "js-sys", + "libc", + "n0-error", + "n0-future", + "n0-watcher", + "netdev 0.45.1", + "netdev 0.46.3", + "netlink-packet-core 0.8.2", + "netlink-packet-route 0.31.0", + "netlink-proto", + "netlink-sys 0.8.8", + "noq-udp", + "objc2-core-foundation", + "objc2-system-configuration", + "pin-project-lite", + "serde", + "socket2", + "time", + "tokio", + "tokio-util", + "tracing", + "web-sys", + "windows 0.62.2", + "windows-result 0.4.1", + "wmi", +] + +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + +[[package]] +name = "noq" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6249a065adc1edbc1704a95436f38dd60476064dbc3b7c47cf279e02d1590eb5" +dependencies = [ + "bytes", + "cfg_aliases", + "derive_more", + "noq-proto", + "noq-udp", + "pin-project-lite", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.20", + "tokio", + "tokio-stream", + "tracing", + "web-time", +] + +[[package]] +name = "noq-proto" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a86d3146ed3995b5913c414f6664344b9617457320782e64f0bb44afd49d74" +checksum = "c01a35be3ed37558c8db69554419f24eff8a63599b54f711bbe3863ec727c47d" +dependencies = [ + "aes-gcm", + "bytes", + "derive_more", + "enum-assoc", + "getrandom 0.4.3", + "identity-hash", + "lru-slab", + "rand 0.10.3", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "sorted-index-buffer", + "thiserror 2.0.20", + "tinyvec", + "tracing", + "web-time", +] [[package]] -name = "mio" -version = "1.2.3" +name = "noq-udp" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +checksum = "8d0160422832eea1cc11c34be04ee6d5a046452b0b46cfb14695d55a1f24c954" dependencies = [ + "cfg_aliases", "libc", - "wasi", + "socket2", + "tracing", "windows-sys 0.61.2", ] [[package]] -name = "nanorand" -version = "0.7.0" +name = "ntapi" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" dependencies = [ - "getrandom 0.2.17", + "winapi", ] [[package]] @@ -1150,6 +2649,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "num-integer" version = "0.1.47" @@ -1189,12 +2694,140 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", + "block2", + "dispatch2", + "libc", + "objc2", +] + +[[package]] +name = "objc2-core-wlan" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e34919aba0d701380d911702455038a8a3587467fe0141d6a71501e7ffe48" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-security", + "objc2-security-foundation", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-security-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef76382e9cedd18123099f17638715cc3d81dba3637d4c0d39ab69df2ef345a5" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "bitflags", + "dispatch2", + "libc", + "objc2", + "objc2-core-foundation", + "objc2-security", +] + [[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl-probe" version = "0.2.1" @@ -1207,6 +2840,22 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" +[[package]] +name = "papaya" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da2442474a9404698c42509b8967f437249dbc7b50493e83020333d3943ec0ae" +dependencies = [ + "equivalent", + "seize", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -1230,12 +2879,57 @@ dependencies = [ "windows-link", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pharos" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" +dependencies = [ + "futures", + "rustc_version", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -1248,8 +2942,18 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der 0.8.2", + "spki 0.8.1", ] [[package]] @@ -1258,6 +2962,93 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" +[[package]] +name = "plist" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327a4cb2ad49dc48adfdc9da89e22c64d488b508c67a044d71ba729e4521a285" +dependencies = [ + "base64 0.23.1", + "indexmap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" +dependencies = [ + "serde", +] + +[[package]] +name = "portmapper" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca97242f016e090a25330613bc2382aab65eb22f9149dbe84d8f8e711d49a530" +dependencies = [ + "base64 0.22.1", + "bytes", + "derive_more", + "hyper-util", + "igd-next", + "iroh-metrics", + "libc", + "n0-error", + "n0-future", + "netwatch", + "num_enum", + "rand 0.10.3", + "serde", + "smallvec", + "socket2", + "time", + "tokio", + "tokio-util", + "tower-layer", + "tracing", + "url", +] + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "postcard-derive", + "serde", +] + +[[package]] +name = "postcard-derive" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0232bd009a197ceec9cc881ba46f727fcd8060a2d8d6a9dde7a69030a6fe2bb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "potential_utf" version = "0.1.6" @@ -1267,6 +3058,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1276,6 +3073,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -1285,6 +3091,30 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quanta" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + +[[package]] +name = "quick-xml" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.47" @@ -1314,7 +3144,18 @@ checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ "libc", "rand_chacha", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", ] [[package]] @@ -1324,7 +3165,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -1336,6 +3177,30 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1441,12 +3306,14 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", ] @@ -1465,13 +3332,10 @@ dependencies = [ ] [[package]] -name = "rustc_version" -version = "0.2.3" +name = "rustc-hash" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" -dependencies = [ - "semver 0.9.0", -] +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -1479,17 +3343,32 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver 1.0.28", + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.45" +version = "0.23.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" dependencies = [ "aws-lc-rs", + "log", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -1514,6 +3393,7 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ + "web-time", "zeroize", ] @@ -1525,7 +3405,7 @@ checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation", "core-foundation-sys", - "jni", + "jni 0.22.4", "log", "once_cell", "rustls", @@ -1562,6 +3442,12 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -1580,6 +3466,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -1610,13 +3502,10 @@ dependencies = [ ] [[package]] -name = "semver" -version = "0.9.0" +name = "seize" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -dependencies = [ - "semver-parser", -] +checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" [[package]] name = "semver" @@ -1625,10 +3514,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] -name = "semver-parser" -version = "0.7.0" +name = "send_wrapper" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "serde" @@ -1640,6 +3529,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_bytes" version = "0.11.19" @@ -1694,12 +3594,36 @@ dependencies = [ ] [[package]] -name = "sha1" -version = "0.6.1" +name = "serde_path_to_error" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" dependencies = [ - "sha1_smol", + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct", + "serde", ] [[package]] @@ -1715,8 +3639,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", ] [[package]] @@ -1750,7 +3685,16 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "rand_core 0.10.1", ] [[package]] @@ -1759,7 +3703,7 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" dependencies = [ - "rustc_version 0.4.1", + "rustc_version", "simdutf8", ] @@ -1769,12 +3713,36 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "simple-dns" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6f884fa9a8d48101774bfbd3aeb81e968dd22cffd19a372da69f183db22c1a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "sketches-ddsketch" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c" + [[package]] name = "slab" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.16.1" @@ -1792,71 +3760,62 @@ dependencies = [ ] [[package]] -name = "spki" -version = "0.7.3" +name = "sorted-index-buffer" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +checksum = "ea06cc588e43c632923a55450401b8f25e628131571d4e1baea1bdfdb2b5ed06" + +[[package]] +name = "spez" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87e960f4dca2788eeb86bbdde8dd246be8948790b7618d656e68f9b720a86e8" dependencies = [ - "base64ct", - "der", + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] -name = "stable_deref_trait" -version = "1.2.1" +name = "spin" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" [[package]] -name = "stdweb" -version = "0.4.20" +name = "spinning_top" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" dependencies = [ - "discard", - "rustc_version 0.2.3", - "serde", - "serde_json", - "stdweb-derive", - "stdweb-internal-macros", - "stdweb-internal-runtime", - "wasm-bindgen", + "lock_api", ] [[package]] -name = "stdweb-derive" -version = "0.5.3" +name = "spki" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ - "proc-macro2", - "quote", - "serde", - "serde_derive", - "syn 1.0.109", + "base64ct", + "der 0.7.10", ] [[package]] -name = "stdweb-internal-macros" -version = "0.2.9" +name = "spki" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +checksum = "7ef958a98b9d5da290cfc78946e9f3e61e1e62a18db0d92cac0b83cc161491a9" dependencies = [ - "base-x", - "proc-macro2", - "quote", - "serde", - "serde_derive", - "serde_json", - "sha1", - "syn 1.0.109", + "base64ct", + "der 0.8.2", ] [[package]] -name = "stdweb-internal-runtime" -version = "0.1.5" +name = "stable_deref_trait" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "strum" @@ -1864,7 +3823,16 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros", + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros 0.28.0", ] [[package]] @@ -1881,22 +3849,23 @@ dependencies = [ ] [[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "1.0.109" +name = "strum_macros" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" dependencies = [ + "heck", "proc-macro2", "quote", - "unicode-ident", + "syn 2.0.119", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.119" @@ -1939,6 +3908,75 @@ dependencies = [ "syn 3.0.6", ] +[[package]] +name = "sysinfo" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c33cd241af0f2e9e3b5c32163b873b29956890b5342e6745b917ce9d490f4af" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "windows 0.57.0", +] + +[[package]] +name = "system-configuration" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501336eb7ba9e417300a6a0fa985721065467aa83a6dcf0422a8e43e4c0328fa" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thehum" +version = "0.33.1" +dependencies = [ + "anyhow", + "async-trait", + "chrono", + "ed25519-dalek 2.2.0", + "ensemble", + "hex", + "hum-identity", + "hum-paths", + "parking_lot", + "serde", + "serde_json", + "sha2 0.10.9", + "thiserror 1.0.69", + "thrum-core", + "tokio", + "tracing", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -1998,11 +4036,42 @@ dependencies = [ "parking_lot", "serde", "serde_json", - "sha2", - "strum", + "sha2 0.10.9", + "strum 0.26.3", "thiserror 1.0.69", ] +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "js-sys", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.4" @@ -2013,6 +4082,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + [[package]] name = "tokio" version = "1.53.1" @@ -2052,17 +4127,83 @@ dependencies = [ ] [[package]] -name = "tokio-util" -version = "0.7.19" +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-websockets" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52efb639344a7c6adb8e62c6f3d2c19c001ff1b79a5041ba1c6ed42e19c6aa5" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-sink", + "getrandom 0.4.3", + "http", + "httparse", + "rand 0.10.3", + "ring", + "rustls-pki-types", + "sha1_smol", + "simdutf8", + "tokio", + "tokio-rustls", + "tokio-util", +] + +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.17+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "libc", - "pin-project-lite", - "tokio", + "winnow", ] [[package]] @@ -2078,6 +4219,7 @@ dependencies = [ "tokio", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -2116,6 +4258,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2195,6 +4338,28 @@ version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -2211,6 +4376,7 @@ dependencies = [ "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -2301,6 +4467,8 @@ dependencies = [ "cfg-if", "once_cell", "rustversion", + "serde", + "serde_json", "wasm-bindgen-macro", "wasm-bindgen-shared", ] @@ -2347,6 +4515,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.105" @@ -2357,6 +4538,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-root-certs" version = "1.0.9" @@ -2366,6 +4557,37 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webpki-roots" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5f280bbb2b287bc8e7de7e3852945a0ff50d1bbcb5ed42b6c7a568c546df171" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -2375,19 +4597,193 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core 0.62.2", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link", + "windows-result 0.4.1", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result 0.4.1", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -2399,34 +4795,76 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -2439,42 +4877,124 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "wmi" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c81b85c57a57500e56669586496bf2abd5cf082b9d32995251185d105208b64" +dependencies = [ + "chrono", + "futures", + "log", + "serde", + "thiserror 2.0.20", + "windows 0.62.2", + "windows-core 0.62.2", +] + [[package]] name = "writeable" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" +[[package]] +name = "ws_stream_wasm" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c173014acad22e83f16403ee360115b38846fe754e735c5d9d3803fe70c6abc" +dependencies = [ + "async_io_stream", + "futures", + "js-sys", + "log", + "pharos", + "rustc_version", + "send_wrapper", + "thiserror 2.0.20", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "xml-rs" +version = "0.8.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" + +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] + [[package]] name = "yoke" version = "0.8.3" @@ -2544,6 +5064,20 @@ name = "zeroize" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] [[package]] name = "zerotrie" diff --git a/hives/bp7/Cargo.toml b/hives/bp7/Cargo.toml index 962b88e8..fbc8ae73 100644 --- a/hives/bp7/Cargo.toml +++ b/hives/bp7/Cargo.toml @@ -16,7 +16,12 @@ serde_json = "1" anyhow = "1" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } -bundle-protocol = { package = "bp7", version = "0.10" } +bundle-protocol = { package = "bp7", version = "0.12" } +sha2 = "0.10" + +[dev-dependencies] +humd = { path = "../../humd" } +tempfile = "3" [[bin]] name = "bp7-forager" diff --git a/hives/bp7/Orchfile b/hives/bp7/Orchfile index 42d030df..cf624d10 100644 --- a/hives/bp7/Orchfile +++ b/hives/bp7/Orchfile @@ -1,4 +1,20 @@ +# bp7: hum as a DTN endpoint. Set an ARG in the environment of +# `hum hive bp7 install` to change it, for example +# BP7_ROUTE=router.local:4556. An empty ARG keeps the forager's own +# default; the README says what each one is. + ARG HUM_SRC= +ARG BP7_LISTEN= +ARG BP7_NODE_EID= +ARG BP7_LABEL= +ARG BP7_MODEL= +ARG BP7_ROUTE= +ARG BP7_ALLOW= +ARG BP7_BLOOMS= +ARG BP7_MTU= +ARG BP7_SPOOL= +ARG BP7_HOLD= +ARG BP7_SWEEP_MS= SERVICE hive-install RUN ${HUM_SRC}/install @@ -6,4 +22,15 @@ ONESHOT true SERVICE bp7 RUN ${HUM_SRC}/target/release/bp7-forager +ENV BP7_LISTEN=${BP7_LISTEN} +ENV BP7_NODE_EID=${BP7_NODE_EID} +ENV BP7_LABEL=${BP7_LABEL} +ENV BP7_MODEL=${BP7_MODEL} +ENV BP7_ROUTE=${BP7_ROUTE} +ENV BP7_ALLOW=${BP7_ALLOW} +ENV BP7_BLOOMS=${BP7_BLOOMS} +ENV BP7_MTU=${BP7_MTU} +ENV BP7_SPOOL=${BP7_SPOOL} +ENV BP7_HOLD=${BP7_HOLD} +ENV BP7_SWEEP_MS=${BP7_SWEEP_MS} RESTART always diff --git a/hives/bp7/README.md b/hives/bp7/README.md index 7b6c5c9d..a4834884 100644 --- a/hives/bp7/README.md +++ b/hives/bp7/README.md @@ -1,109 +1,161 @@ --- title: "bp7-forager (Rust)" -description: "RFC 9171 Bundle Protocol v7 — interplanetary store-and-forward bridge to hum" +description: "RFC 9171 Bundle Protocol v7: hum as a DTN endpoint, and space's way into the ensemble" --- # bp7-forager -> _the interplanetary bee — accept BP7 bundles from spacecraft and other DTN nodes; reply via the same store-and-forward path_ +> _the interplanetary bee: take a bundle from any DTN node, hold it until hum can answer, and send the answer home the way bundles go_ -Hum, but over **DTN**. The Delay/Disruption-Tolerant Networking -protocol used by NASA's ION, ESA's tests on the ISS, and the planned -lunar Gateway. RFC 9171. CBOR on the wire. Designed for the actual -solar system: one-way delays measured in minutes (Mars) to hours -(Voyager). +Hum, but over **DTN**: the Delay/Disruption-Tolerant Networking of NASA's +ION and HDTN, ESA's tests on the ISS, and the planned lunar Gateway. +RFC 9171, CBOR on the wire, built for links where one-way delay is +minutes (Mars) to hours (Voyager) and no end-to-end path exists at any +given moment. A bundle addressed to `dtn://hum.local/inference` arrives over UDP -(or any other convergence layer your DTN router provides). We decode, -extract the payload, hand it to humd as `chi:"prompt"`, collect the -bloom's text, wrap the answer in a reply bundle, and send it back to the -bundle's source EID. Real protocol; works today against a real DTN -router on your laptop. +(RFC 7122) from a DTN router or node. The forager holds it on disk, asks +humd for a bloom, and sends the bloom's text back to the bundle's source +endpoint as a bundle of its own. The bloom need not grow here: humd hands +it to whichever nurse grows the model, on this humd or on a trusted peer +across the ensemble, so a node in space reaches every nurse the +ensemble has. ## Propensity | statefulness | richness | wire shape | hides | |---|---|---|---| -| stateless per-call | lean | RFC 9171 BP7 over UDPCL | tools, system, perf, drone, breath | +| holds each ask until it is answered or expires | lean | RFC 9171 BP7 over UDPCL | tools, perf, breath | ## Wire ``` -DTN peer ──BP7 bundle (CBOR over UDP:4556)──► bp7-forager ──chi:"prompt"──► humd - │ -DTN peer ◄──BP7 bundle (CBOR over UDP)──── bp7-forager ◄──chi:"wilt"──────────┘ +DTN node ─bundle─► router ─UDP:4556─► bp7-forager ─► spool ─chi:"prompt"─► humd ─► nurse, here or across the ensemble + │ │ +DTN node ◄─bundle─ router ◄─BP7_ROUTE─────┘◄──────────── chi:"wilt" ────────┘ ``` -Payload may be: - -- **Plain UTF-8 text** — treated as the prompt body -- **JSON** — `{ "text": "...", "modelId": "claude-sonnet-4", "system": "..." }` - -The bundle's source EID is reused as the reply destination. Reply -payload is the bloom's unfurled text concatenated into a single -UTF-8 body. No SSE re-framing — DTN replies are *messages*, not -streams. +**An ask** is a bundle whose payload is either plain UTF-8 text, taken +as the prompt, or JSON: `{"text": "...", "modelId": "claude-sonnet-4", +"system": "..."}`. + +**The answer** is one bundle to the ask's source endpoint, or several +fragments of one when it is longer than `BP7_MTU`. Its payload is the +bloom's text, or what broke when the bloom broke off. Every answer +carries block 193 (RFC 9171 §4.3.2 leaves 192 to 255 for private use), +whose data is `{"hum": "reply", "to": "", "ok": +true|false}`: a DTN node that knows hum can match an answer to its ask, +and one that does not can ignore the block. Every block has a CRC-32 +(§4.3.1). The answer lives as long as the ask was given to live, asks +for no status reports, and carries a hop limit of 32. + +## What it holds to + +- **Store and forward.** An ask is on disk before anything is done with + it, and stays until its answer has gone home. A restart of the forager + or of humd loses nothing; the bloom is grown again. +- **Heard once.** DTN delivers at least once. A bundle's id is its source + and creation timestamp, the same in every copy and every fragment, and + a copy of an ask held or answered is dropped until the ask expires. +- **Lifetime.** A bundle past its lifetime is deleted (§5.5), and so is + an ask whose lifetime runs out while it waits. A node with no clock + sends creation time 0 and a bundle age block (§4.4.2), and its asks + expire by their age. The ask's expiry rides on the prompt as `dusk`. +- **Fragments.** An ask that arrives in pieces, in any order, with + overlaps and repeats, is grown once from the whole (§5.9). An answer + too long for one datagram is fragmented at the bundle layer, block 193 + in every fragment (§5.8), so no datagram needs IP fragmentation + (RFC 7122 §3.2). +- **What is not an ask.** A bundle that fails its CRC or RFC 9171's + checks, is for another endpoint, is an administrative record, is + hum's own answer, or names no source (`dtn:none`) is never grown, so + no two hums, and no status report, can keep a loop of blooms going. +- **No nurse yet.** An ask for a model no nurse grows waits, retried + with backoff, until one comes or the ask expires. So does one whose + humd is away. +- **Bounded.** At most `BP7_BLOOMS` blooms at once, and at most + `BP7_HOLD` asks held; past that a bundle is dropped, as a node with + depleted storage does. An ask is at most 4 MiB, however many pieces + it arrives in. ## Configure | env | default | what | |---|---|---| -| `BP7_LISTEN` | `0.0.0.0:4556` | UDP convergence-layer address | -| `BP7_NODE_EID` | `dtn://hum.local/inference` | this node's endpoint id (URI). Bundles addressed elsewhere are dropped | -| `BP7_MODEL` | `claude-sonnet-4` | model humd spawns when a payload doesn't specify one | -| `HUM_THRUM_SOCK` | `$XDG_STATE_HOME/hum/thrum.sock` | humd's NDJSON socket | +| `BP7_LISTEN` | `0.0.0.0:4556` | the UDP convergence-layer address (IANA's 4556) | +| `BP7_NODE_EID` | `dtn://hum.local/inference` | this endpoint; a bundle for any other is not ours | +| `BP7_LABEL` | `main` | the bee's label; give each forager on one humd its own | +| `BP7_MODEL` | `claude-sonnet-4` | the model when an ask names none | +| `BP7_ROUTE` | the address the bundle came from | where every answer goes: the router's UDPCL address, `host:port` | +| `BP7_ALLOW` | anyone | the IPs a bundle is taken from, comma-separated (RFC 7122 §5) | +| `BP7_BLOOMS` | `4` | blooms grown at once | +| `BP7_MTU` | `1232` | the largest datagram an answer goes in; 1232 fits any IPv6 path | +| `BP7_SPOOL` | `$XDG_STATE_HOME/hum/hives/bp7/spool` | where asks are held | +| `BP7_HOLD` | `10000` | asks held at once | +| `BP7_SWEEP_MS` | `60000` | how often what has expired is forgotten | + +Behind a router, set `BP7_ROUTE`: a router sends from a port of its own +choosing, not the one it listens on, so the address a bundle came from is +rarely where an answer can go. Without a router, a node that sends from +the socket it listens on gets its answer there. ## Run ```bash -cargo run -p bp7 --bin bp7-forager +hum hive bp7 install # build it, enter it, keep it running +BP7_ROUTE=router.local:4556 hum hive bp7 install # behind a router ``` -Hit it from another machine running [NASA's ION](https://sourceforge.net/projects/ion-dtn/), -[µPCN](https://upcn.eu/), or [hDTN](https://github.com/nasa/HDTN): +The forager is a forager bee of the hive `bp7`; its hive must be +installed on the humd it lands on. For the ensemble to grow its asks on +another humd, that humd must be a peer, its nurse's hive installed here, +and this humd must trust remote nurses (`trustRemoteNurses` in +`hum.json`, or `HUM_TRUST_REMOTE_NURSES=1`). + +## Test ```bash -# example with bp7-rs's own bin from your laptop -bp7-rs --src dtn://you/ \ - --dst dtn://hum.local/inference \ - --listen 0.0.0.0:4557 \ - --send '{"text":"What is the speed of light in glass?"}' +cd hives/bp7 && cargo test ``` -The reply bundle lands on your listening port whenever round-trip -latency permits. On a LAN: ~milliseconds. Through one TDRS relay -hop: ~600ms. Real Mars round-trip: 5–24 minutes. - -## What this actually unlocks - -- **A hum node addressable from any DTN-speaking peer in the solar system.** A spacecraft on Mars (or its simulation in your lab) can prompt your humd by store-and-forwarding a bundle that includes your EID. The reply rides the same path home. -- **Hum-the-protocol over physically partitioned links.** thrum was designed assuming local sockets are reliable; DTN was designed assuming *no* path exists at any given moment. The two layered together is a genuine partition-tolerant agent system. -- **Custody transfer**. A real DTN router holds your bundle, with custody, until the next hop confirms receipt. If a Mars probe drops the link mid-payload, the upstream router resends on the next contact window. The bp7-forager itself doesn't implement custody (we're a leaf node); the surrounding DTN router does. - -## What this bee does NOT do - -- **Route**. Bundles addressed to other EIDs get dropped, not forwarded. Run a real DTN router (ION / µPCN / hDTN) alongside this bee and point its routing table at our UDP port if you want routing. -- **Authenticate**. BP7 has security extensions (BPSec); this v1 doesn't use them. Anyone reachable on UDP:4556 can prompt your humd. -- **Custody transfer**. We acknowledge a bundle by replying — that's the only signal upstream gets. A real DTN deployment layers custody on top. -- **Stream**. DTN is store-and-forward, not streaming. Your `chi:"unfurl"` tones get collected into one reply bundle. If you want progress-via-bundle, send multiple intermediate bundles — that's a future extension. - -## Why this is the right kind of unhinged - -Hum was designed for local sockets and millisecond peer-to-peer. DTN -was designed for one-way delays of minutes to hours, intermittent -connectivity, and custody-transfer at every hop. Bridging the two -exposes every assumption hum makes about timing. The drone's `lost` -threshold fires after 30s; a Mars round trip is 600× that. The wane -counter ticks for a wall-clock minute and finds nothing. Your humd -discovers what it actually feels like to be a peer 90M km away. - -The thought experiment: aim a humd at an actual Mars rover ground -station and see what falls off. (Don't — DSN time is contested.) +The tests are the forager as a DTN node meets it, against a real humd: +`tests/dtn.rs` covers duplicates, bursts, expiry, fragments both ways, +corruption, status reports, loops, routing and restarts; `tests/outlive.rs` +covers humd going away; `tests/ensemble.rs` has an ask from space grown by +a nurse on another humd, over TCP. `tests/common` holds a minimal DTN node, +which is also the shortest example of speaking to the forager. + +## What is not yet + +- **An ask whose grower leaves the ensemble waits out its lifetime.** + When humd hands a bloom to a peer's nurse and that peer goes away + mid-bloom, humd removes the peer but never breaks off the blooms it + was growing, so the forager hears neither a wilt nor an error and + cannot grow the ask again until the bundle expires, which may be days + for an ask from deep space. humd also keeps which peer grew each bloom + after it wilts. The fix belongs in humd: on losing a peer, send each + bloom it was growing an `error` a forager can retry on, and forget a + bloom's grower when it wilts. `tests/grower_gone.rs` is the test for + it, ignored until then. +- **No BPSec.** Anyone who can reach the UDP port can prompt this humd; + keep it behind a router, and set `BP7_ALLOW`. `bp7` 0.12 signs and + verifies BIB-HMAC-SHA2 (RFC 9172, 9173), so authenticated asks are the + next step. +- **No custody, no status reports.** The forager is a leaf: it sends no + reception or delivery reports, even when an ask asks for them. The + router around it does custody. +- **UDP only.** TCPCL and LTP are the router's to speak. +- **One answer per ask.** DTN answers are messages, not streams; the + unfurls are gathered into one answer. +- **humd keeps every sid a connection opens until the connection + closes.** The forager holds one connection for its life, so humd's + memory grows a little with every bloom it grows. ## See also -- [RFC 9171 — Bundle Protocol v7](https://datatracker.ietf.org/doc/rfc9171/) -- [NASA ION](https://sourceforge.net/projects/ion-dtn/) — the reference DTN router -- [bp7-rs](https://github.com/dtn7/bp7-rs) — the Rust crate this bee uses -- [Vint Cerf, "An Internet for Mars"](https://www.youtube.com/watch?v=hKt-jK19vHU) — the architecture talk that started this -- [THRUM.md](../../THRUM.md) — the underlying thrum protocol +- [RFC 9171](https://datatracker.ietf.org/doc/rfc9171/): Bundle Protocol v7 +- [RFC 7122](https://datatracker.ietf.org/doc/rfc7122/): datagram convergence layers +- [RFC 9172](https://datatracker.ietf.org/doc/rfc9172/): BPSec +- [bp7-rs](https://github.com/dtn7/bp7-rs): the Rust crate this bee uses +- [NASA ION](https://sourceforge.net/projects/ion-dtn/) and [HDTN](https://github.com/nasa/HDTN): DTN routers +- [THRUM.md](../../THRUM.md): the protocol on humd's side diff --git a/hives/bp7/examples/humd.rs b/hives/bp7/examples/humd.rs new file mode 100644 index 00000000..25744891 --- /dev/null +++ b/hives/bp7/examples/humd.rs @@ -0,0 +1,28 @@ +//! A humd for the tests: the daemon as the `humd` binary runs it, under +//! whatever roots its environment names, until it is told to stop. The +//! ensemble test runs one of these as the humd a DTN node's ask lands on, +//! beside the humd in the test's own process. + +use tokio::signal::unix::{signal, SignalKind}; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + hum_paths::init(); + tracing_subscriber::fmt() + .with_ansi(false) + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_env("HUM_LOG_LEVEL") + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), + ) + .init(); + let cfg = humd::DaemonConfig::from_env(); + humd::run(cfg, async { + let mut term = signal(SignalKind::terminate()).expect("SIGTERM"); + let mut int = signal(SignalKind::interrupt()).expect("SIGINT"); + tokio::select! { + _ = term.recv() => {} + _ = int.recv() => {} + } + }) + .await +} diff --git a/hives/bp7/src/dtn.rs b/hives/bp7/src/dtn.rs new file mode 100644 index 00000000..e878448f --- /dev/null +++ b/hives/bp7/src/dtn.rs @@ -0,0 +1,272 @@ +//! The bundle side: what a datagram that arrives is, and the bundles an +//! answer goes home in. +//! +//! An arrival is an ask, a piece of one, or dropped for a reason. It is +//! dropped when it does not decode, when a CRC fails, when RFC 9171 does +//! not hold for it, when it is for another endpoint, when it is an +//! administrative record, when it is hum's own answer, when it names no +//! source to answer, and when its lifetime is over (§5.5). Its id is the +//! source and creation timestamp, which is the same for every piece and +//! every copy of it. +//! +//! An answer goes home as one bundle to the asker's endpoint, with a CRC +//! on every block (§4.3.1), the lifetime the ask was given, no status +//! reports asked for, and block 193 saying it answers the ask's id. An +//! answer longer than the datagram allows is cut into fragments that each +//! fit (§5.8; RFC 7122 §3.2), every one of them carrying block 193. + +use std::time::Duration; + +use anyhow::{bail, Result}; +use bundle_protocol::canonical::{ + new_hop_count_block, new_payload_block, CanonicalBlock, CanonicalBlockBuilder, CanonicalData, + BUNDLE_AGE_BLOCK, +}; +use bundle_protocol::crc::CRC_32; +use bundle_protocol::flags::{BlockControlFlags, BundleControlFlags}; +use bundle_protocol::primary::PrimaryBlockBuilder; +use bundle_protocol::{Bundle, CreationTimestamp, EndpointID}; +use serde_json::{json, Value}; + +pub const HUM_BLOCK: u64 = 193; +pub const HOP_LIMIT: u8 = 32; +pub const MOST_ASK: u64 = 4 * 1024 * 1024; +const MS_1970_TO_2000: u64 = 946_684_800_000; +const MOST_HEADER: usize = 5; + +#[derive(Debug, Clone, PartialEq)] +pub struct Ask { + pub id: String, + pub source: String, + pub expires: u64, + pub lifetime_ms: u64, +} + +#[derive(Debug)] +pub enum Arrival { + Whole(Ask, Vec), + Piece { ask: Ask, offset: u64, total: u64, data: Vec }, + Dropped(&'static str), +} + +pub fn arrive(bytes: &[u8], node_eid: &str, now_ms: u64) -> Arrival { + let Ok(mut bundle) = Bundle::try_from(bytes) else { + return Arrival::Dropped("undecodable"); + }; + if !bundle.crc_valid() { + return Arrival::Dropped("crc"); + } + if bundle.validate().is_err() { + return Arrival::Dropped("invalid"); + } + if bundle.primary.destination.to_string() != node_eid { + return Arrival::Dropped("not-ours"); + } + if bundle.is_administrative_record() { + return Arrival::Dropped("administrative-record"); + } + if is_reply(&bundle) { + return Arrival::Dropped("hum-reply"); + } + if bundle.primary.source == EndpointID::none() { + return Arrival::Dropped("anonymous"); + } + let lifetime_ms = bundle.primary.lifetime.as_millis() as u64; + let created = bundle.primary.creation_timestamp.dtntime(); + let expires = if created != 0 { + MS_1970_TO_2000.saturating_add(created).saturating_add(lifetime_ms) + } else { + now_ms.saturating_add(lifetime_ms).saturating_sub(age_ms(&bundle)) + }; + if expires <= now_ms { + return Arrival::Dropped("expired"); + } + let Some(data) = bundle.payload().cloned() else { + return Arrival::Dropped("no-payload"); + }; + let ts = &bundle.primary.creation_timestamp; + let source = bundle.primary.source.to_string(); + let ask = Ask { id: format!("{source}-{}-{}", ts.dtntime(), ts.seqno()), source, expires, lifetime_ms }; + if bundle.primary.has_fragmentation() { + let offset = bundle.primary.fragmentation_offset; + let total = bundle.primary.total_data_length; + if offset.saturating_add(data.len() as u64) > total { + return Arrival::Dropped("piece-outside-its-whole"); + } + if total > MOST_ASK { + return Arrival::Dropped("too-large"); + } + return Arrival::Piece { ask, offset, total, data }; + } + Arrival::Whole(ask, data) +} + +fn age_ms(bundle: &Bundle) -> u64 { + bundle + .canonicals + .iter() + .find(|b| b.block_type == BUNDLE_AGE_BLOCK) + .and_then(|b| match b.data() { + CanonicalData::BundleAge(ms) => Some(*ms), + _ => None, + }) + .unwrap_or(0) +} + +fn is_reply(bundle: &Bundle) -> bool { + bundle.canonicals.iter().any(|b| { + b.block_type == HUM_BLOCK + && matches!(b.data(), CanonicalData::Unknown(bytes) + if serde_json::from_slice::(bytes).is_ok_and(|v| v["hum"] == "reply")) + }) +} + +pub struct Answer<'a> { + pub to: &'a str, + pub answers: &'a str, + pub ok: bool, + pub text: &'a [u8], + pub lifetime: Duration, +} + +pub fn reply(node_eid: &str, answer: &Answer, mtu: usize) -> Result>> { + let source = EndpointID::try_from(node_eid).map_err(|e| anyhow::anyhow!("{node_eid}: {e:?}"))?; + let dest = EndpointID::try_from(answer.to).map_err(|e| anyhow::anyhow!("{}: {e:?}", answer.to))?; + let mark = serde_json::to_vec(&json!({"hum": "reply", "to": answer.answers, "ok": answer.ok}))?; + let created = CreationTimestamp::now(); + let one = |fragment: Option<(u64, &[u8])>| -> Vec { + let mut primary = PrimaryBlockBuilder::default() + .destination(dest.clone()) + .source(source.clone()) + .report_to(EndpointID::none()) + .creation_timestamp(created.clone()) + .lifetime(answer.lifetime); + let data = match fragment { + Some((offset, data)) => { + primary = primary + .bundle_control_flags(BundleControlFlags::BUNDLE_IS_FRAGMENT.bits()) + .fragmentation_offset(offset) + .total_data_length(answer.text.len() as u64); + data + } + None => answer.text, + }; + let mut bundle = Bundle::new( + primary.build().expect("a reply's primary block is whole"), + vec![ + new_payload_block(BlockControlFlags::empty(), data.to_vec()), + new_hop_count_block(2, BlockControlFlags::BLOCK_REPLICATE, HOP_LIMIT), + mark_block(&mark), + ], + ); + bundle.set_crc(CRC_32); + bundle.sort_canonicals(); + bundle.to_cbor() + }; + let whole = one(None); + if whole.len() <= mtu { + return Ok(vec![whole]); + } + let overhead = one(Some((answer.text.len() as u64, &[]))).len() + MOST_HEADER; + if mtu <= overhead { + bail!("BP7_MTU {mtu} leaves no room for an answer beside its {overhead} bytes of blocks"); + } + let chunk = mtu - overhead; + Ok(answer + .text + .chunks(chunk) + .enumerate() + .map(|(i, data)| one(Some(((i * chunk) as u64, data)))) + .collect()) +} + +fn mark_block(mark: &[u8]) -> CanonicalBlock { + CanonicalBlockBuilder::default() + .block_type(HUM_BLOCK) + .block_number(3) + .block_control_flags(BlockControlFlags::BLOCK_REPLICATE.bits()) + .data(CanonicalData::Unknown(mark.to_vec())) + .build() + .expect("block 193 is whole") +} + +#[cfg(test)] +mod tests { + use super::*; + + const ME: &str = "dtn://hum.test/inference"; + const YOU: &str = "dtn://you.test/"; + + fn now() -> u64 { + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis() as u64 + } + + fn answer(text: &[u8]) -> Answer<'_> { + Answer { to: YOU, answers: "dtn://you.test/-1-0", ok: true, text, lifetime: Duration::from_secs(60) } + } + + #[test] + fn a_reply_is_heard_as_a_reply() { + let bytes = reply(ME, &answer(b"hi"), 1232).unwrap().remove(0); + let mut b = Bundle::try_from(bytes.as_slice()).unwrap(); + assert!(b.crc_valid() && b.validate().is_ok()); + b.primary.destination = EndpointID::try_from(ME).unwrap(); + b.calculate_crc(); + assert!(matches!(arrive(&b.to_cbor(), ME, now()), Arrival::Dropped("hum-reply"))); + } + + #[test] + fn every_fragment_fits_and_they_cover_the_answer() { + let text = vec![b'x'; 10_000]; + for mtu in [300, 1232, 4000] { + let datagrams = reply(ME, &answer(&text), mtu).unwrap(); + assert!(datagrams.len() > 1); + let mut covered = 0; + for d in &datagrams { + assert!(d.len() <= mtu, "{} > {mtu}", d.len()); + let b = Bundle::try_from(d.as_slice()).unwrap(); + assert_eq!(b.primary.fragmentation_offset as usize, covered); + assert_eq!(b.primary.total_data_length as usize, text.len()); + covered += b.payload().unwrap().len(); + } + assert_eq!(covered, text.len()); + } + } + + #[test] + fn an_mtu_too_small_for_the_blocks_is_an_error() { + assert!(reply(ME, &answer(&[b'x'; 500]), 40).is_err()); + } + + #[test] + fn a_clockless_ask_expires_by_its_age() { + let primary = PrimaryBlockBuilder::default() + .destination(EndpointID::try_from(ME).unwrap()) + .source(EndpointID::try_from(YOU).unwrap()) + .report_to(EndpointID::none()) + .creation_timestamp(CreationTimestamp::with_time_and_seq(0, 7)) + .lifetime(Duration::from_secs(10)) + .build() + .unwrap(); + let with_age = |age_ms: Option| { + let mut blocks = vec![new_payload_block(BlockControlFlags::empty(), b"q".to_vec())]; + if let Some(ms) = age_ms { + blocks.push(bundle_protocol::canonical::new_bundle_age_block(2, BlockControlFlags::empty(), ms)); + } + let mut b = Bundle::new(primary.clone(), blocks); + b.set_crc(CRC_32); + b.sort_canonicals(); + b.to_cbor() + }; + let t = now(); + match arrive(&with_age(Some(4_000)), ME, t) { + Arrival::Whole(ask, _) => assert_eq!(ask.expires, t + 6_000), + other => panic!("{other:?}"), + } + assert!(matches!(arrive(&with_age(Some(11_000)), ME, t), Arrival::Dropped("expired"))); + assert!( + matches!(arrive(&with_age(None), ME, t), Arrival::Dropped("invalid")), + "RFC 9171 4.4.2: a bundle with no creation time carries its age" + ); + } +} diff --git a/hives/bp7/src/hum.rs b/hives/bp7/src/hum.rs new file mode 100644 index 00000000..bd35aa39 --- /dev/null +++ b/hives/bp7/src/hum.rs @@ -0,0 +1,196 @@ +//! The hum side: one forager bee for the life of the process, and the +//! blooms it opens for the asks the spool holds. +//! +//! The bee is linked once and outlives humd as every bee does; a bloom is +//! opened only while it is nestled, since a tone sent while humd is away +//! is dropped. Each bloom has its own sid, made from the ask's key and +//! the attempt, so no two asks, and no two tries at one ask, ever share a +//! line of blooms; its mid is the same, and its dusk is the ask's expiry. +//! What humd sounds under a sid goes to whoever opened it. humd going +//! away ends every open bloom as astray, and an ask on it is tried again. + +use std::collections::HashMap; +use std::time::Duration; + +use hum_thrum::{Bee, Event, Options, Sender}; +use serde_json::{json, Map, Value}; +use thrum_core::{Chi, PetalKind}; +use tokio::sync::{mpsc, watch}; +use tracing::{error, info, warn}; + +pub enum Heard { + Unfurl(String), + Wilt, + Broke { code: Option, message: String }, + Astray, +} + +enum Cmd { + Open { sid: String, rid: String, prompt: Value, to: mpsc::UnboundedSender }, + Close { sid: String }, +} + +#[derive(Clone)] +pub struct Hum { + cmds: mpsc::UnboundedSender, + nestled: watch::Receiver, + sender: Sender, +} + +pub struct Bloom<'a> { + pub sid: String, + pub text: &'a str, + pub model: &'a str, + pub system: Option<&'a str>, + pub dusk: u64, +} + +pub enum Outcome { + Grown { ok: bool, text: String }, + Again(String), + Expired, +} + +impl Hum { + pub fn start(bee: Bee) -> Hum { + let (cmds, rx) = mpsc::unbounded_channel(); + let (nestled_tx, nestled) = watch::channel(false); + let sender = bee.sender(); + tokio::spawn(dispatch(bee, rx, nestled_tx)); + Hum { cmds, nestled, sender } + } + + pub async fn grow(&self, bloom: Bloom<'_>, deadline: tokio::time::Instant) -> Outcome { + let mut nestled = self.nestled.clone(); + let here = tokio::time::timeout_at(deadline, nestled.wait_for(|n| *n)).await.is_ok_and(|r| r.is_ok()); + if !here { + return Outcome::Expired; + } + let rid = format!("{}-ask", bloom.sid); + let mut prompt = Map::new(); + prompt.insert("chi".into(), json!(Chi::Prompt)); + prompt.insert("rid".into(), json!(rid)); + prompt.insert("sid".into(), json!(bloom.sid)); + prompt.insert("mid".into(), json!(bloom.sid)); + prompt.insert("dusk".into(), json!(bloom.dusk)); + prompt.insert("text".into(), json!(bloom.text)); + prompt.insert("modelId".into(), json!(bloom.model)); + if let Some(system) = bloom.system { + prompt.insert("systemPrompt".into(), json!(system)); + } + let (to, mut heard) = mpsc::unbounded_channel(); + let _ = self.cmds.send(Cmd::Open { sid: bloom.sid.clone(), rid, prompt: Value::Object(prompt), to }); + let mut text = String::new(); + let outcome = loop { + let next = tokio::time::timeout_at(deadline, heard.recv()).await; + match next { + Err(_) => { + self.sender.send(json!({"chi": Chi::Cancel, "sid": bloom.sid})); + break Outcome::Expired; + } + Ok(None) => break Outcome::Again("the bee stopped".into()), + Ok(Some(Heard::Unfurl(more))) => text.push_str(&more), + Ok(Some(Heard::Wilt)) => break Outcome::Grown { ok: true, text }, + Ok(Some(Heard::Broke { code, message })) if code.as_deref() == Some("no-nurse") => { + break Outcome::Again(message); + } + Ok(Some(Heard::Broke { message, .. })) => break Outcome::Grown { ok: false, text: message }, + Ok(Some(Heard::Astray)) => break Outcome::Again("humd went away mid-bloom".into()), + } + }; + let _ = self.cmds.send(Cmd::Close { sid: bloom.sid }); + outcome + } +} + +async fn dispatch(mut bee: Bee, mut cmds: mpsc::UnboundedReceiver, nestled: watch::Sender) { + let mut open: HashMap> = HashMap::new(); + let mut rids: HashMap = HashMap::new(); + let mut here = false; + loop { + tokio::select! { + cmd = cmds.recv() => match cmd { + Some(Cmd::Open { sid, rid, prompt, to }) => { + if !here { + let _ = to.send(Heard::Astray); + continue; + } + open.insert(sid.clone(), to); + rids.insert(rid, sid); + bee.send(prompt); + } + Some(Cmd::Close { sid }) => { + open.remove(&sid); + rids.retain(|_, s| *s != sid); + } + None => return, + }, + event = bee.next() => match event { + Some(Event::Nestled) => { + here = true; + info!("bp7.nestled"); + let _ = nestled.send(true); + } + Some(Event::Astray) => { + here = false; + warn!(open = open.len(), "bp7.astray — humd went away; the asks it held are tried again"); + let _ = nestled.send(false); + for (_, to) in open.drain() { + let _ = to.send(Heard::Astray); + } + rids.clear(); + } + Some(Event::Refused(why)) => { + error!(%why, "bp7.refused"); + std::process::exit(3); + } + Some(Event::Advisory(a)) => info!(code = %a.code, "bp7.advisory"), + Some(Event::Tone(tone)) => route(&tone, &open, &rids), + None => { + error!("bp7.client.stopped"); + std::process::exit(1); + } + }, + } + } +} + +fn route(tone: &Value, open: &HashMap>, rids: &HashMap) { + let chi = tone.get("chi").and_then(Value::as_str).unwrap_or(""); + if chi == "echo" { + if tone.get("ok") == Some(&Value::Bool(false)) + && let Some(sid) = tone.get("rid").and_then(Value::as_str).and_then(|r| rids.get(r)) + && let Some(to) = open.get(sid) + { + let why = tone.get("error").and_then(Value::as_str).unwrap_or("humd refused the ask"); + let _ = to.send(Heard::Broke { code: None, message: why.to_string() }); + } + return; + } + let Some(to) = tone.get("sid").and_then(Value::as_str).and_then(|s| open.get(s)) else { + return; + }; + let heard = match chi { + "unfurl" if tone.get("kind").and_then(Value::as_str) == Some(PetalKind::Text.as_str()) => { + Heard::Unfurl(tone.get("text").and_then(Value::as_str).unwrap_or("").to_string()) + } + "wilt" => Heard::Wilt, + "error" => Heard::Broke { + code: tone.get("code").and_then(Value::as_str).map(str::to_string), + message: tone.get("message").and_then(Value::as_str).unwrap_or("the bloom broke off").to_string(), + }, + _ => return, + }; + let _ = to.send(heard); +} + +pub fn backoff(attempts: u32) -> Duration { + Duration::from_millis(500u64.saturating_mul(1 << attempts.min(6))).min(Duration::from_secs(30)) +} + +pub fn link(label: &str, hello: Value) -> anyhow::Result { + let opts = Options::new(crate::HIVE_NAME, label, thrum_core::Role::Forager).hello(hello); + let key = hum_identity::enter_as(crate::HIVE_NAME, label, thrum_core::Role::Forager.as_str()) + .map_err(|e| hum_thrum::Refused(e.to_string()))?; + Ok(Bee::spawn(opts, key)) +} diff --git a/hives/bp7/src/main.rs b/hives/bp7/src/main.rs index eb39435f..5c5a5eb5 100644 --- a/hives/bp7/src/main.rs +++ b/hives/bp7/src/main.rs @@ -1,201 +1,346 @@ -use std::net::SocketAddr; -use std::sync::Arc; +//! bp7-forager: hum as a DTN endpoint. A bundle that asks arrives over +//! UDP (RFC 7122), is held in the spool until its answer has gone home, +//! and is answered by a bloom on the humd here. +//! +//! Three tasks share the spool. Intake decodes what arrives and holds +//! what is an ask, off the socket's task so a contact window's burst is +//! not lost to a full receive buffer. Growers take held asks, at most +//! `BP7_BLOOMS` at a time, open a bloom for each, and send its answer +//! home; an ask that cannot bloom yet, because humd is away or no nurse +//! grows its model, is tried again until it expires. The sweeper forgets +//! what has expired. -use anyhow::{anyhow, Context, Result}; +mod dtn; +mod hum; +mod spool; + +use std::collections::HashSet; +use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use anyhow::{anyhow, bail, Context, Result}; use serde_json::{json, Value}; -use hum_thrum::{Bee, Event, Options, Refused}; -use thrum_core::{Chi, PetalKind}; use tokio::net::UdpSocket; -use tracing::{info, warn}; +use tokio::sync::{mpsc, Semaphore}; +use tracing::{debug, info, warn}; -const HIVE_NAME: &str = "bp7"; -const NESTLING_VERSION: &str = env!("CARGO_PKG_VERSION"); -const DEFAULT_NODE_EID: &str = "dtn://hum.local/inference"; -const DEFAULT_MODEL: &str = "claude-sonnet-4"; -const BUNDLE_BUFFER_BYTES: usize = 65536; +use crate::dtn::Arrival; +use crate::hum::{Bloom, Hum, Outcome}; +use crate::spool::{Grown, Spool, Taken}; -const DEFAULT_LISTEN: &str = "0.0.0.0:4556"; +pub const HIVE_NAME: &str = "bp7"; +const NESTLING_VERSION: &str = env!("CARGO_PKG_VERSION"); +const DATAGRAM: usize = 65_536; +const INTAKE: usize = 4_096; #[derive(Debug, Clone)] struct Config { listen: SocketAddr, node_eid: String, + label: String, model: String, + route: Option, + allow: Option>, + blooms: usize, + mtu: usize, + spool: PathBuf, + hold: usize, + sweep: Duration, +} + +fn var(name: &str) -> Option { + std::env::var(name).ok().filter(|v| !v.trim().is_empty()) +} + +fn parsed(name: &str, default: T) -> Result +where + T::Err: std::fmt::Display, +{ + match var(name) { + Some(v) => v.trim().parse().map_err(|e| anyhow!("{name}={v}: {e}")), + None => Ok(default), + } } impl Config { fn from_env() -> Result { - let listen_str = std::env::var("BP7_LISTEN").unwrap_or_else(|_| DEFAULT_LISTEN.into()); + let node_eid = var("BP7_NODE_EID").unwrap_or_else(|| "dtn://hum.local/inference".into()); + bundle_protocol::EndpointID::try_from(node_eid.as_str()) + .map_err(|e| anyhow!("BP7_NODE_EID={node_eid} is not an endpoint id: {e:?}"))?; + let route = match var("BP7_ROUTE") { + Some(r) => Some( + r.to_socket_addrs() + .with_context(|| format!("BP7_ROUTE={r}"))? + .next() + .ok_or_else(|| anyhow!("BP7_ROUTE={r} resolves to nothing"))?, + ), + None => None, + }; + let allow = match var("BP7_ALLOW") { + Some(list) => Some( + list.split(',') + .map(|ip| ip.trim().parse().map_err(|e| anyhow!("BP7_ALLOW: {ip}: {e}"))) + .collect::>>()?, + ), + None => None, + }; + let blooms: usize = parsed("BP7_BLOOMS", 4)?; + if blooms == 0 { + bail!("BP7_BLOOMS must be at least 1"); + } Ok(Self { - listen: listen_str.parse().with_context(|| format!("parse BP7_LISTEN={listen_str}"))?, - node_eid: std::env::var("BP7_NODE_EID").unwrap_or_else(|_| DEFAULT_NODE_EID.into()), - model: std::env::var("BP7_MODEL").unwrap_or_else(|_| DEFAULT_MODEL.into()), + listen: parsed("BP7_LISTEN", "0.0.0.0:4556".parse()?)?, + node_eid, + label: var("BP7_LABEL").unwrap_or_else(|| "main".into()), + model: var("BP7_MODEL").unwrap_or_else(|| "claude-sonnet-4".into()), + route, + allow, + blooms, + mtu: parsed("BP7_MTU", 1232)?, + spool: var("BP7_SPOOL").map(PathBuf::from).unwrap_or_else(|| hum_paths::hive_state_dir(HIVE_NAME).join("spool")), + hold: parsed("BP7_HOLD", 10_000)?, + sweep: Duration::from_millis(parsed("BP7_SWEEP_MS", 60_000)?), }) } } +fn now_ms() -> u64 { + SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_millis() as u64).unwrap_or(0) +} + +fn deadline_of(expires: u64) -> tokio::time::Instant { + tokio::time::Instant::now() + Duration::from_millis(expires.saturating_sub(now_ms())) +} + +struct Forager { + cfg: Config, + sock: Arc, + spool: Mutex, + hum: Hum, + queue: mpsc::UnboundedSender, + busy: Mutex>, +} + #[tokio::main] -async fn main() -> Result<()> { +async fn main() { hum_paths::init(); tracing_subscriber::fmt() + .with_ansi(std::io::IsTerminal::is_terminal(&std::io::stdout())) .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), ) .init(); - let cfg = Arc::new(Config::from_env()?); - let sock = UdpSocket::bind(cfg.listen).await - .with_context(|| format!("bind {}", cfg.listen))?; - let sock = Arc::new(sock); - info!(listen = %cfg.listen, eid = %cfg.node_eid, "bp7-forager.listen"); - - let mut buf = vec![0u8; BUNDLE_BUFFER_BYTES]; - loop { - let (n, peer) = sock.recv_from(&mut buf).await?; - let bytes = buf[..n].to_vec(); - let cfg = cfg.clone(); - let sock = sock.clone(); - tokio::spawn(async move { - if let Err(e) = handle_bundle(cfg, sock, peer, bytes).await { - warn!(error = ?e, %peer, "bundle.handle"); - } - }); + if let Err(e) = run().await { + let refused = e.downcast_ref::().is_some(); + tracing::error!(error = %format!("{e:#}"), "bp7.stopped"); + eprintln!("bp7-forager: {e:#}"); + std::process::exit(if refused { 3 } else { 1 }); } } -async fn handle_bundle( - cfg: Arc, - sock: Arc, - peer: SocketAddr, - bytes: Vec, -) -> Result<()> { - let bndl = bundle_protocol::Bundle::try_from(bytes.as_slice()) - .map_err(|e| anyhow!("bp7 decode: {e:?}"))?; - - let dest = bndl.primary.destination.to_string(); - let src = bndl.primary.source.to_string(); - if !addressed_to_us(&dest, &cfg.node_eid) { - info!(%dest, %src, "bundle.drop.not-for-us"); - return Ok(()); - } - - let payload = bndl.payload().ok_or_else(|| anyhow!("bundle has no payload block"))?; - let prompt = parse_payload(payload, &cfg.model); - info!(%src, %dest, len = prompt.text.len(), "bundle.recv"); - - let reply_text = run_prompt(&cfg, &prompt).await?; - let mut reply_bundle = build_reply(&cfg.node_eid, &src, reply_text.as_bytes()); - - let cbor = reply_bundle.to_cbor(); - sock.send_to(&cbor, peer).await - .with_context(|| format!("reply send to {peer}"))?; - info!(%src, dest = %src, bytes = cbor.len(), "bundle.send.reply"); - Ok(()) -} +async fn run() -> Result<()> { + let cfg = Config::from_env()?; + let spool = Spool::open(&cfg.spool, cfg.hold)?; + let bee = hum::link(&cfg.label, json!({ + "version": NESTLING_VERSION, + "propensity": {"statefulness": "stateless", "richness": "lean", "wire": "bp7/dtn-udpcl"}, + "chis": ["hello", "prompt", "cancel", "unfurl", "wilt", "error"], + "source": "https://github.com/adiled/hum/tree/main/hives/bp7", + }))?; + let sock = Arc::new(UdpSocket::bind(cfg.listen).await.with_context(|| format!("bind {}", cfg.listen))?); + let (queue, asks) = mpsc::unbounded_channel(); + let me = Arc::new(Forager { cfg, sock, spool: Mutex::new(spool), hum: Hum::start(bee), queue, busy: Mutex::default() }); + info!( + listen = %me.sock.local_addr()?, eid = %me.cfg.node_eid, label = %me.cfg.label, route = ?me.cfg.route, + blooms = me.cfg.blooms, mtu = me.cfg.mtu, spool = %me.cfg.spool.display(), + "bp7.listen" + ); -fn addressed_to_us(dest: &str, node_eid: &str) -> bool { - dest == node_eid || dest.starts_with(node_eid) -} + me.sweep(); + for key in me.spool.lock().unwrap().waiting() { + me.enqueue(key); + } + tokio::spawn(me.clone().grow_all(asks)); + tokio::spawn({ + let me = me.clone(); + async move { + loop { + tokio::time::sleep(me.cfg.sweep).await; + me.sweep(); + } + } + }); -struct Payload { - text: String, - model: String, - system: Option, + let (arrived, intake) = mpsc::channel::<(Vec, SocketAddr)>(INTAKE); + tokio::task::spawn_blocking({ + let me = me.clone(); + move || me.intake(intake) + }); + let mut buf = vec![0u8; DATAGRAM]; + loop { + let (n, peer) = me.sock.recv_from(&mut buf).await?; + if let Some(allow) = &me.cfg.allow + && !allow.contains(&peer.ip()) + { + debug!(%peer, "bundle.drop.not-allowed"); + continue; + } + if arrived.try_send((buf[..n].to_vec(), peer)).is_err() { + warn!(%peer, "bundle.drop.intake-full — the forager is taking bundles slower than they arrive"); + } + } } -fn parse_payload(payload: &[u8], default_model: &str) -> Payload { - if let Ok(v) = serde_json::from_slice::(payload) { - if let Some(text) = v.get("text").and_then(Value::as_str) { - let model = v.get("modelId").and_then(Value::as_str) - .unwrap_or(default_model).to_string(); - let system = v.get("system").and_then(Value::as_str).map(str::to_string); - return Payload { text: text.to_string(), model, system }; +impl Forager { + fn intake(self: Arc, mut arrived: mpsc::Receiver<(Vec, SocketAddr)>) { + while let Some((bytes, peer)) = arrived.blocking_recv() { + let taken = match dtn::arrive(&bytes, &self.cfg.node_eid, now_ms()) { + Arrival::Dropped(why) => { + info!(%peer, why, "bundle.drop"); + continue; + } + Arrival::Whole(ask, payload) => { + let taken = self.spool.lock().unwrap().take(&ask, &payload, peer); + (ask, taken) + } + Arrival::Piece { ask, offset, total, data } => { + let taken = self.spool.lock().unwrap().piece(&ask, offset, total, &data, peer); + (ask, taken) + } + }; + match taken { + (ask, Ok(Taken::New(key))) => { + info!(id = %ask.id, key, "bundle.held"); + self.enqueue(key); + } + (ask, Ok(Taken::Partial)) => debug!(id = %ask.id, "bundle.piece.held"), + (ask, Ok(Taken::Duplicate)) => info!(id = %ask.id, "bundle.drop.duplicate"), + (ask, Ok(Taken::Full)) => warn!(id = %ask.id, "bundle.drop.spool-full"), + (ask, Err(e)) => warn!(id = %ask.id, error = %format!("{e:#}"), "bundle.drop.spool-failed"), + } } } - Payload { - text: String::from_utf8_lossy(payload).into_owned(), - model: default_model.to_string(), - system: None, - } -} -fn unfurled_text(tone: &Value) -> Option<&str> { - if tone.get("kind").and_then(Value::as_str) != Some(PetalKind::Text.as_str()) { - return None; + fn enqueue(&self, key: String) { + if self.busy.lock().unwrap().insert(key.clone()) { + let _ = self.queue.send(key); + } } - tone.get("text").and_then(Value::as_str) -} -fn build_reply(my_eid: &str, source_eid: &str, payload: &[u8]) -> bundle_protocol::Bundle { - let src = bundle_protocol::EndpointID::try_from(my_eid).expect("our EID parses"); - let dst = bundle_protocol::EndpointID::try_from(source_eid).unwrap_or_else(|_| { - warn!(source_eid, "could not parse source EID; reply will go to dtn:none"); - bundle_protocol::EndpointID::none() - }); - bundle_protocol::bundle::new_std_payload_bundle(src, dst, payload.to_vec()) -} + fn sweep(&self) { + for key in self.spool.lock().unwrap().forget_expired(now_ms()) { + info!(key, "bundle.expired.unanswered"); + } + } -async fn run_prompt(_cfg: &Config, payload: &Payload) -> Result { - let hello = json!({ - "version": NESTLING_VERSION, - "propensity": { - "statefulness": "stateless", - "richness": "lean", - "wire": "bp7/dtn-udpcl", - }, - "chis": ["hello", "prompt", "unfurl", "wilt", "error"], - "source": "https://github.com/adiled/hum/tree/main/hives/bp7", - }); - let mut bee = Bee::link(Options::new(HIVE_NAME, "main", thrum_core::Role::Forager).hello(hello)) - .await - .context("link the bp7 bee")?; - - let sid = format!("bp7-{}", now_ms()); - let mut prompt = serde_json::Map::new(); - prompt.insert("chi".into(), json!(Chi::Prompt)); - prompt.insert("rid".into(), Value::String(format!("p-{sid}"))); - prompt.insert("mid".into(), Value::String(hum_identity::HumId::mint().to_string())); - prompt.insert("sid".into(), Value::String(sid.clone())); - prompt.insert("text".into(), Value::String(payload.text.clone())); - prompt.insert("modelId".into(), Value::String(payload.model.clone())); - if let Some(system) = payload.system.as_ref() { - prompt.insert("systemPrompt".into(), Value::String(system.clone())); + async fn grow_all(self: Arc, mut asks: mpsc::UnboundedReceiver) { + let room = Arc::new(Semaphore::new(self.cfg.blooms)); + while let Some(key) = asks.recv().await { + let permit = room.clone().acquire_owned().await.expect("the semaphore stays open"); + let me = self.clone(); + tokio::spawn(async move { + let again = me.grow(&key).await; + drop(permit); + me.busy.lock().unwrap().remove(&key); + if let Some(after) = again { + tokio::time::sleep(after).await; + me.enqueue(key); + } + }); + } } - bee.send(Value::Object(prompt)); - - let mut collected = String::new(); - let answer = loop { - let tone = match bee.next().await { - Some(Event::Tone(tone)) => tone, - Some(Event::Astray) => break Err(anyhow!("humd went away mid-bloom")), - Some(Event::Refused(why)) => break Err(Refused(why).into()), - Some(Event::Nestled | Event::Advisory(_)) => continue, - None => break Err(anyhow!("the client stopped mid-bloom")), + + async fn grow(&self, key: &str) -> Option { + let loaded = self.spool.lock().unwrap().load(key); + let Ok((held, payload, grown)) = loaded else { + return None; }; - if tone.get("sid").and_then(Value::as_str) != Some(sid.as_str()) { - continue; + if held.expires <= now_ms() { + info!(id = %held.id, "bundle.expired.unanswered"); + let _ = self.spool.lock().unwrap().heard(key, held.expires); + return None; } - match tone.get("chi").and_then(Value::as_str) { - Some("unfurl") => { - if let Some(text) = unfurled_text(&tone) { - collected.push_str(text); + let grown = match grown { + Some(grown) => grown, + None => { + let attempt = match self.spool.lock().unwrap().attempt(key) { + Ok(n) => n, + Err(e) => { + warn!(id = %held.id, error = %format!("{e:#}"), "bundle.spool.failed"); + return Some(hum::backoff(held.attempts)); + } + }; + let (text, model, system) = words(&payload, &self.cfg.model); + let bloom = Bloom { + sid: format!("bp7-{}-{attempt}", &key[..16]), + text: &text, + model: &model, + system: system.as_deref(), + dusk: held.expires, + }; + match self.hum.grow(bloom, deadline_of(held.expires)).await { + Outcome::Grown { ok, text } => { + let grown = Grown { ok, text }; + if let Err(e) = self.spool.lock().unwrap().grown(key, &grown) { + warn!(id = %held.id, error = %format!("{e:#}"), "bundle.spool.failed"); + } + grown + } + Outcome::Again(why) => { + info!(id = %held.id, attempt, %why, "bundle.again"); + return Some(hum::backoff(attempt)); + } + Outcome::Expired => { + info!(id = %held.id, "bundle.expired.unanswered"); + let _ = self.spool.lock().unwrap().heard(key, held.expires); + return None; + } } } - Some("wilt") => break Ok(collected), - Some("error") => { - let msg = tone.get("message").and_then(Value::as_str).unwrap_or("stream error"); - break Err(anyhow!("humd error: {msg}")); + }; + match self.send_home(&held, &grown).await { + Ok(n) => { + info!(id = %held.id, ok = grown.ok, bundles = n, "bundle.answered"); + let _ = self.spool.lock().unwrap().heard(key, held.expires); + None + } + Err(e) => { + warn!(id = %held.id, error = %format!("{e:#}"), "bundle.send.failed"); + Some(hum::backoff(held.attempts)) } - _ => {} } - }; - bee.close(); - answer + } + + async fn send_home(&self, held: &spool::Held, grown: &Grown) -> Result { + let datagrams = dtn::reply( + &self.cfg.node_eid, + &dtn::Answer { + to: &held.source, + answers: &held.id, + ok: grown.ok, + text: grown.text.as_bytes(), + lifetime: Duration::from_millis(held.lifetime_ms), + }, + self.cfg.mtu, + )?; + let to = self.cfg.route.unwrap_or(held.peer); + for d in &datagrams { + self.sock.send_to(d, to).await.with_context(|| format!("send to {to}"))?; + } + Ok(datagrams.len()) + } } -fn now_ms() -> i64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_millis() as i64) - .unwrap_or(0) +fn words(payload: &[u8], default_model: &str) -> (String, String, Option) { + if let Ok(v) = serde_json::from_slice::(payload) + && let Some(text) = v.get("text").and_then(Value::as_str) + { + let model = v.get("modelId").and_then(Value::as_str).unwrap_or(default_model); + let system = v.get("system").and_then(Value::as_str).map(str::to_string); + return (text.to_string(), model.to_string(), system); + } + (String::from_utf8_lossy(payload).into_owned(), default_model.to_string(), None) } diff --git a/hives/bp7/src/spool.rs b/hives/bp7/src/spool.rs new file mode 100644 index 00000000..9d90087a --- /dev/null +++ b/hives/bp7/src/spool.rs @@ -0,0 +1,307 @@ +//! The spool: what the forager holds between a bundle arriving and its +//! answer leaving, on disk, so neither a restart of the forager nor one +//! of humd loses an ask a DTN node handed over. +//! +//! ```text +//! asks/.json an ask held: who asked, when it expires, which bloom it is on +//! asks/.payload its words, as they arrived +//! asks/.answer its answer, once grown and until it has gone home +//! pieces// the pieces of an ask that arrived fragmented +//! heard/ an ask answered or let go, kept until it expires +//! ``` +//! +//! A key is the sha-256 of the ask's id. The `.json` is written last, so +//! an ask is held once it exists. An ask is heard once: while it is held +//! or in `heard/`, a copy of it, whole or in pieces, is a duplicate, and +//! once it has expired no copy can be taken anyway, so what is past its +//! expiry is forgotten. The spool holds at most `hold` asks and pieced +//! asks at a time; past that it takes nothing new. + +use std::collections::BTreeMap; +use std::fs; +use std::net::SocketAddr; +use std::path::{Path, PathBuf}; + +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +use crate::dtn::Ask; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Held { + pub id: String, + pub source: String, + pub expires: u64, + pub lifetime_ms: u64, + pub peer: SocketAddr, + #[serde(default)] + pub attempts: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Grown { + pub ok: bool, + pub text: String, +} + +#[derive(Debug, PartialEq)] +pub enum Taken { + New(String), + Partial, + Duplicate, + Full, +} + +pub struct Spool { + root: PathBuf, + hold: usize, + held: usize, +} + +pub fn key(id: &str) -> String { + Sha256::digest(id.as_bytes()).iter().take(16).map(|b| format!("{b:02x}")).collect() +} + +fn write(path: &Path, bytes: &[u8]) -> Result<()> { + hum_paths::layout::write_atomic_private(path, bytes).with_context(|| format!("write {}", path.display()))?; + if let Some(dir) = path.parent() { + fs::File::open(dir).and_then(|d| d.sync_all()).ok(); + } + Ok(()) +} + +fn names(dir: &Path) -> Vec { + fs::read_dir(dir) + .map(|rd| rd.filter_map(Result::ok).filter_map(|e| e.file_name().into_string().ok()).collect()) + .unwrap_or_default() +} + +fn expires_of(path: &Path) -> Option { + fs::read_to_string(path).ok()?.trim().parse().ok() +} + +impl Spool { + pub fn open(root: &Path, hold: usize) -> Result { + for sub in ["asks", "pieces", "heard"] { + fs::create_dir_all(root.join(sub)).with_context(|| format!("create {}", root.join(sub).display()))?; + } + let mut spool = Spool { root: root.to_path_buf(), hold, held: 0 }; + spool.held = spool.waiting().len() + names(&spool.root.join("pieces")).len(); + Ok(spool) + } + + fn ask_path(&self, key: &str, ext: &str) -> PathBuf { + self.root.join("asks").join(format!("{key}.{ext}")) + } + + fn heard_path(&self, key: &str) -> PathBuf { + self.root.join("heard").join(key) + } + + fn pieces_dir(&self, key: &str) -> PathBuf { + self.root.join("pieces").join(key) + } + + fn known(&self, key: &str) -> bool { + self.heard_path(key).exists() || self.ask_path(key, "json").exists() + } + + pub fn take(&mut self, ask: &Ask, payload: &[u8], peer: SocketAddr) -> Result { + let key = key(&ask.id); + if self.known(&key) { + return Ok(Taken::Duplicate); + } + if self.held >= self.hold { + return Ok(Taken::Full); + } + self.hold_ask(&key, ask, payload, peer)?; + self.held += 1; + Ok(Taken::New(key)) + } + + fn hold_ask(&self, key: &str, ask: &Ask, payload: &[u8], peer: SocketAddr) -> Result<()> { + let held = Held { + id: ask.id.clone(), + source: ask.source.clone(), + expires: ask.expires, + lifetime_ms: ask.lifetime_ms, + peer, + attempts: 0, + }; + write(&self.ask_path(key, "payload"), payload)?; + write(&self.ask_path(key, "json"), &serde_json::to_vec(&held)?) + } + + pub fn piece(&mut self, ask: &Ask, offset: u64, total: u64, data: &[u8], peer: SocketAddr) -> Result { + let key = key(&ask.id); + if self.known(&key) { + return Ok(Taken::Duplicate); + } + let dir = self.pieces_dir(&key); + if !dir.exists() { + if self.held >= self.hold { + return Ok(Taken::Full); + } + fs::create_dir_all(&dir)?; + write(&dir.join("expires"), ask.expires.to_string().as_bytes())?; + write(&dir.join("total"), total.to_string().as_bytes())?; + self.held += 1; + } + if expires_of(&dir.join("total")) != Some(total) { + return Ok(Taken::Partial); + } + let at = dir.join(format!("{offset}-{}", data.len())); + if !at.exists() { + write(&at, data)?; + } + let mut spans: BTreeMap = BTreeMap::new(); + for name in names(&dir) { + if let Some((off, len)) = name.split_once('-').and_then(|(o, l)| Some((o.parse::().ok()?, l.parse::().ok()?))) { + let end = off + len; + let e = spans.entry(off).or_insert(end); + *e = (*e).max(end); + } + } + let mut reach = 0u64; + for (off, end) in &spans { + if *off > reach { + break; + } + reach = reach.max(*end); + } + if reach < total { + return Ok(Taken::Partial); + } + let mut whole = vec![0u8; total as usize]; + for name in names(&dir) { + if let Some(off) = name.split_once('-').and_then(|(o, _)| o.parse::().ok()) { + let bytes = fs::read(dir.join(&name))?; + whole[off..off + bytes.len()].copy_from_slice(&bytes); + } + } + self.hold_ask(&key, ask, &whole, peer)?; + fs::remove_dir_all(&dir).ok(); + Ok(Taken::New(key)) + } + + pub fn waiting(&self) -> Vec { + names(&self.root.join("asks")) + .into_iter() + .filter_map(|n| n.strip_suffix(".json").map(str::to_string)) + .collect() + } + + pub fn load(&self, key: &str) -> Result<(Held, Vec, Option)> { + let held: Held = serde_json::from_slice(&fs::read(self.ask_path(key, "json"))?)?; + let payload = fs::read(self.ask_path(key, "payload"))?; + let grown = fs::read(self.ask_path(key, "answer")).ok().and_then(|b| serde_json::from_slice(&b).ok()); + Ok((held, payload, grown)) + } + + pub fn attempt(&self, key: &str) -> Result { + let (mut held, _, _) = self.load(key)?; + held.attempts += 1; + write(&self.ask_path(key, "json"), &serde_json::to_vec(&held)?)?; + Ok(held.attempts) + } + + pub fn grown(&self, key: &str, grown: &Grown) -> Result<()> { + write(&self.ask_path(key, "answer"), &serde_json::to_vec(grown)?) + } + + pub fn heard(&mut self, key: &str, expires: u64) -> Result<()> { + write(&self.heard_path(key), expires.to_string().as_bytes())?; + self.release(key); + Ok(()) + } + + fn release(&mut self, key: &str) { + let had = self.ask_path(key, "json").exists(); + for ext in ["json", "payload", "answer"] { + fs::remove_file(self.ask_path(key, ext)).ok(); + } + if had { + self.held = self.held.saturating_sub(1); + } + } + + pub fn forget_expired(&mut self, now_ms: u64) -> Vec { + let mut let_go = vec![]; + for key in names(&self.root.join("heard")) { + if expires_of(&self.heard_path(&key)).is_none_or(|e| e <= now_ms) { + fs::remove_file(self.heard_path(&key)).ok(); + } + } + for key in names(&self.root.join("pieces")) { + let dir = self.pieces_dir(&key); + if expires_of(&dir.join("expires")).is_none_or(|e| e <= now_ms) { + fs::remove_dir_all(&dir).ok(); + self.held = self.held.saturating_sub(1); + } + } + for key in self.waiting() { + if self.load(&key).map(|(h, _, _)| h.expires <= now_ms).unwrap_or(true) { + self.release(&key); + let_go.push(key); + } + } + let_go + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn ask(id: &str, expires: u64) -> Ask { + Ask { id: id.into(), source: "dtn://you/".into(), expires, lifetime_ms: 60_000 } + } + + fn peer() -> SocketAddr { + "127.0.0.1:4556".parse().unwrap() + } + + #[test] + fn an_ask_is_taken_once_until_it_expires() { + let dir = tempfile::tempdir().unwrap(); + let mut s = Spool::open(dir.path(), 10).unwrap(); + let a = ask("a", 1_000); + let Taken::New(k) = s.take(&a, b"q", peer()).unwrap() else { panic!() }; + assert_eq!(s.take(&a, b"q", peer()).unwrap(), Taken::Duplicate); + s.heard(&k, a.expires).unwrap(); + assert_eq!(s.take(&a, b"q", peer()).unwrap(), Taken::Duplicate); + assert!(s.forget_expired(999).is_empty()); + assert_eq!(s.take(&a, b"q", peer()).unwrap(), Taken::Duplicate); + s.forget_expired(1_000); + assert!(matches!(s.take(&a, b"q", peer()).unwrap(), Taken::New(_))); + } + + #[test] + fn pieces_make_a_whole_whatever_their_order_and_overlap() { + let dir = tempfile::tempdir().unwrap(); + let mut s = Spool::open(dir.path(), 10).unwrap(); + let a = ask("p", u64::MAX); + let whole = b"0123456789abcdef"; + assert_eq!(s.piece(&a, 10, 16, &whole[10..], peer()).unwrap(), Taken::Partial); + assert_eq!(s.piece(&a, 0, 16, &whole[..6], peer()).unwrap(), Taken::Partial); + assert_eq!(s.piece(&a, 0, 16, &whole[..6], peer()).unwrap(), Taken::Partial); + assert_eq!(s.piece(&a, 0, 9_999, &[b'!'; 30], peer()).unwrap(), Taken::Partial, "a piece of another whole is ignored"); + let Taken::New(k) = s.piece(&a, 4, 16, &whole[4..12], peer()).unwrap() else { panic!() }; + let (_, payload, _) = s.load(&k).unwrap(); + assert_eq!(payload, whole); + assert_eq!(s.piece(&a, 0, 16, &whole[..6], peer()).unwrap(), Taken::Duplicate); + } + + #[test] + fn a_full_spool_takes_nothing_new_and_survives_a_restart() { + let dir = tempfile::tempdir().unwrap(); + let mut s = Spool::open(dir.path(), 2).unwrap(); + assert!(matches!(s.take(&ask("1", u64::MAX), b"q", peer()).unwrap(), Taken::New(_))); + assert_eq!(s.piece(&ask("2", u64::MAX), 0, 9, b"abc", peer()).unwrap(), Taken::Partial); + assert_eq!(s.take(&ask("3", u64::MAX), b"q", peer()).unwrap(), Taken::Full); + let mut again = Spool::open(dir.path(), 2).unwrap(); + assert_eq!(again.waiting().len(), 1); + assert_eq!(again.take(&ask("3", u64::MAX), b"q", peer()).unwrap(), Taken::Full); + } +} diff --git a/hives/bp7/tests/common/mod.rs b/hives/bp7/tests/common/mod.rs new file mode 100644 index 00000000..abea0b4d --- /dev/null +++ b/hives/bp7/tests/common/mod.rs @@ -0,0 +1,517 @@ +//! The world a DTN node sees the forager from: a humd in a root of its +//! own, a nurse that answers by what it is asked, the forager as the +//! process orchd would run, and a node that speaks bundles over UDP. +//! +//! The nurse answers `echo: `, or by the first word of the text: +//! `slow ` waits before it echoes, `big ` unfurls n bytes, +//! `fail` breaks off with an error. + +#![allow(dead_code)] + +pub mod near; + +use std::collections::HashMap; +use std::net::SocketAddr; +use std::path::{Path, PathBuf}; +use std::process::Stdio; +use std::sync::{Arc, Mutex, OnceLock}; +use std::time::{Duration, Instant}; + +use bundle_protocol::canonical::{ + new_hop_count_block, new_payload_block, CanonicalBlockBuilder, CanonicalData, +}; +use bundle_protocol::flags::{BlockControlFlags, BundleControlFlags}; +use bundle_protocol::primary::PrimaryBlockBuilder; +use bundle_protocol::{dtn_time_now, Bundle, CreationTimestamp, EndpointID}; +use hum_thrum::{Bee, Event, Options, Sender}; +use serde_json::{json, Value}; +use thrum_core::Role; +use tokio::net::UdpSocket; + +pub const WAIT: Duration = Duration::from_secs(30); +pub const QUIET: Duration = Duration::from_millis(1500); +pub const HUM_BLOCK: u64 = 193; +pub const MODEL: &str = "echo"; + +#[derive(Clone, Debug)] +pub struct Heard { + pub model: String, + pub text: String, + pub sid: String, +} + +pub struct World { + pub root: PathBuf, + pub ensemble: bool, + heard: Arc>>, + busy: Arc>>, + rt: tokio::runtime::Handle, + humd: Mutex>, +} + +static WORLD: OnceLock = OnceLock::new(); + +pub fn world() -> &'static World { + WORLD.get_or_init(|| World::new(false)) +} + +pub fn ensemble_world() -> &'static World { + let w = WORLD.get_or_init(|| World::new(true)); + assert!(w.ensemble, "one test binary, one kind of world"); + w +} + +impl World { + fn new(ensemble: bool) -> World { + let root = tempfile::Builder::new().prefix("bp7").tempdir().unwrap().keep(); + unsafe { + std::env::set_var("XDG_STATE_HOME", root.join("s")); + std::env::set_var("XDG_CONFIG_HOME", root.join("c")); + std::env::set_var("XDG_DATA_HOME", root.join("d")); + std::env::set_var("XDG_CACHE_HOME", root.join("k")); + std::env::set_var("XDG_RUNTIME_DIR", root.join("r")); + std::env::set_var("HUM_LOG_LEVEL", "warn"); + std::env::remove_var("HUM_THRUM_SOCK"); + std::env::remove_var("HUM_SOCKET"); + } + std::fs::create_dir_all(hum_paths::hives_d_dir()).unwrap(); + let humd = if ensemble { + json!({"humd": {"metricsAddr": "none", "irohPort": 0, "tcpListen": "127.0.0.1:0"}}) + } else { + json!({"humd": {"metricsAddr": "none", "irohPort": 0}}) + }; + std::fs::write(hum_paths::hum_json(), humd.to_string()).unwrap(); + for hive in ["bp7", "nurse-echo", "nurse-later"] { + std::fs::write(hum_paths::hive_installation(hive), "# bp7's test\n").unwrap(); + } + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap(); + tx.send(rt.handle().clone()).unwrap(); + rt.block_on(std::future::pending::<()>()); + }); + let world = World { + root, + ensemble, + heard: Arc::default(), + busy: Arc::default(), + rt: rx.recv().unwrap(), + humd: Mutex::new(Some(Humd::start(ensemble))), + }; + world.nurse("nurse-echo", MODEL); + world + } + + pub fn stop_humd(&self) { + if let Some(mut humd) = self.humd.lock().unwrap().take() { + humd.stop(); + } + } + + pub fn start_humd(&self) { + let mut humd = self.humd.lock().unwrap(); + if humd.is_none() { + *humd = Some(Humd::start(self.ensemble)); + } + } + + pub fn pin_tcp(&self, tcp_hint: &str) { + let addr = tcp_hint.trim_start_matches("tcp:"); + let cfg = json!({"humd": {"metricsAddr": "none", "irohPort": 0, "tcpListen": addr}}); + std::fs::write(hum_paths::hum_json(), cfg.to_string()).unwrap(); + } + + pub fn heard(&self, marker: &str) -> Vec { + self.heard.lock().unwrap().iter().filter(|h| h.text.contains(marker)).cloned().collect() + } + + pub fn peak(&self, group: &str) -> usize { + self.busy.lock().unwrap().get(group).map(|(_, peak)| *peak).unwrap_or(0) + } + + pub fn nurse(&self, hive: &str, model: &str) { + let heard = self.heard.clone(); + let busy = self.busy.clone(); + let (hive, model) = (hive.to_string(), model.to_string()); + let (nestled_tx, nestled_rx) = std::sync::mpsc::channel(); + self.rt.spawn(async move { + let key = hum_identity::enter_as(&hive, "main", "nurse").unwrap(); + let opts = Options::new(&hive, "main", Role::Nurse).hello(json!({ + "version": "1.0.0", + "models": [model], + "headroom": {"freeSlots": 64, "totalSlots": 64}, + })); + let mut bee = Bee::spawn(opts, key); + let sender = bee.sender(); + while let Some(event) = bee.next().await { + let tone = match event { + Event::Nestled => { + let _ = nestled_tx.send(()); + continue; + } + Event::Tone(tone) if tone["chi"] == "prompt" => tone, + _ => continue, + }; + let sid = tone["sid"].as_str().unwrap_or("").to_string(); + let text = tone["text"].as_str().unwrap_or("").to_string(); + let model = tone["modelId"].as_str().unwrap_or("").to_string(); + heard.lock().unwrap().push(Heard { model, text: text.clone(), sid: sid.clone() }); + let group = group_of(&text); + { + let mut busy = busy.lock().unwrap(); + let (now, peak) = busy.entry(group.clone()).or_default(); + *now += 1; + *peak = (*peak).max(*now); + } + let (sender, busy) = (sender.clone(), busy.clone()); + tokio::spawn(async move { + answer(&sender, &sid, &text).await; + if let Some((now, _)) = busy.lock().unwrap().get_mut(&group) { + *now -= 1; + } + }); + } + }); + nestled_rx.recv_timeout(WAIT).expect("the nurse nestles"); + } +} + +fn group_of(text: &str) -> String { + let last = text.split_whitespace().last().unwrap_or(""); + last.rsplit_once('-').map(|(g, _)| g).unwrap_or(last).to_string() +} + +async fn answer(bee: &Sender, sid: &str, text: &str) { + let unfurl = |text: &str| json!({"chi": "unfurl", "sid": sid, "petal": 0, "kind": "text", "text": text}); + let mut words = text.to_string(); + if let Some(rest) = text.strip_prefix("slow ") { + let (ms, rest) = rest.split_once(' ').unwrap_or((rest, "")); + tokio::time::sleep(Duration::from_millis(ms.parse().unwrap_or(0))).await; + words = rest.to_string(); + } + if let Some(n) = text.strip_prefix("big ") { + let n: usize = n.trim().parse().unwrap_or(0); + let mut left = n; + while left > 0 { + let take = left.min(4096); + bee.send(unfurl(&"x".repeat(take))); + left -= take; + } + } else if text == "fail" { + bee.send(json!({"chi": "error", "sid": sid, "code": "boom", "message": "the cell broke"})); + return; + } else { + bee.send(unfurl(&format!("echo: {words}"))); + } + bee.send(json!({"chi": "wilt", "sid": sid, "reason": "stop"})); +} + +struct Humd { + stop: Option>, + thread: Option>, +} + +impl Humd { + fn start(keyed: bool) -> Humd { + let sock = hum_paths::thrum_sock_resolved(); + let _ = std::fs::remove_file(&sock); + let (tx, rx) = tokio::sync::oneshot::channel::<()>(); + let thread = std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap(); + rt.block_on(async { + let mut cfg = humd::DaemonConfig::from_env(); + if !keyed { + cfg.humd_key = None; + } + let _ = humd::run(cfg, async { + let _ = rx.await; + }) + .await; + }); + rt.shutdown_background(); + }); + let since = Instant::now(); + while std::os::unix::net::UnixStream::connect(&sock).is_err() { + assert!(since.elapsed() < WAIT, "humd did not bind {}", sock.display()); + std::thread::sleep(Duration::from_millis(50)); + } + Humd { stop: Some(tx), thread: Some(thread) } + } + + fn stop(&mut self) { + if let Some(tx) = self.stop.take() { + let _ = tx.send(()); + } + if let Some(t) = self.thread.take() { + let _ = t.join(); + } + } +} + +pub struct Forager { + child: tokio::process::Child, + pub name: String, + pub addr: SocketAddr, + pub eid: String, + pub spool: PathBuf, + pub log: PathBuf, + env: Vec<(String, String)>, +} + +impl Forager { + pub async fn start(name: &str, env: &[(&str, &str)]) -> Forager { + let w = world(); + let env = env.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(); + Forager::spawn( + name.to_string(), + "127.0.0.1:0".parse().unwrap(), + format!("dtn://{name}.hum.test/inference"), + w.root.join(format!("spool-{name}")), + env, + 0, + ) + .await + } + + async fn spawn( + name: String, + addr: SocketAddr, + eid: String, + spool: PathBuf, + env: Vec<(String, String)>, + life: u32, + ) -> Forager { + let log = world().root.join(format!("forager-{name}-{life}.log")); + let out = std::fs::File::create(&log).unwrap(); + let mut cmd = tokio::process::Command::new(env!("CARGO_BIN_EXE_bp7-forager")); + cmd.env("BP7_LISTEN", addr.to_string()) + .env("BP7_NODE_EID", &eid) + .env("BP7_MODEL", MODEL) + .env("BP7_SPOOL", &spool) + .env("BP7_LABEL", &name) + .env("RUST_LOG", "info") + .stdout(out.try_clone().unwrap()) + .stderr(out) + .kill_on_drop(true); + for (k, v) in &env { + cmd.env(k, v); + } + let child = cmd.spawn().unwrap(); + let mut forager = Forager { child, name, addr, eid, spool, log, env }; + forager.wait_for("bp7.listen").await; + let log = forager.log_text(); + let bound = log + .split("bp7.listen listen=") + .nth(1) + .and_then(|rest| rest.split_whitespace().next()) + .and_then(|a| a.parse().ok()) + .unwrap_or_else(|| panic!("{}: no bound address in its log:\n{log}", forager.name)); + forager.addr = bound; + forager + } + + pub async fn wait_for(&self, line: &str) { + let since = Instant::now(); + loop { + if std::fs::read_to_string(&self.log).unwrap_or_default().contains(line) { + return; + } + assert!(since.elapsed() < WAIT, "{}: no {line:?} in its log:\n{}", self.name, self.log_text()); + tokio::time::sleep(Duration::from_millis(25)).await; + } + } + + pub fn log_text(&self) -> String { + std::fs::read_to_string(&self.log).unwrap_or_default() + } + + pub async fn restart(mut self) -> Forager { + let _ = self.child.kill().await; + let _ = self.child.wait().await; + let life = std::fs::read_dir(&world().root) + .unwrap() + .filter_map(Result::ok) + .filter(|e| e.file_name().to_string_lossy().starts_with(&format!("forager-{}-", self.name))) + .count() as u32; + let any = "127.0.0.1:0".parse().unwrap(); + Forager::spawn(self.name.clone(), any, self.eid.clone(), self.spool.clone(), self.env.clone(), life).await + } +} + +pub fn eid(s: &str) -> EndpointID { + EndpointID::try_from(s).unwrap() +} + +pub fn ask(from: &str, to: &str, payload: &[u8]) -> Bundle { + ask_with(from, to, payload, Duration::from_secs(3600), CreationTimestamp::now()) +} + +pub fn ask_with(from: &str, to: &str, payload: &[u8], lifetime: Duration, created: CreationTimestamp) -> Bundle { + let source = if from == "dtn:none" { EndpointID::none() } else { eid(from) }; + let primary = PrimaryBlockBuilder::default() + .destination(eid(to)) + .source(source) + .report_to(EndpointID::none()) + .creation_timestamp(created) + .lifetime(lifetime) + .build() + .unwrap(); + let mut b = Bundle::new( + primary, + vec![ + new_payload_block(BlockControlFlags::empty(), payload.to_vec()), + new_hop_count_block(2, BlockControlFlags::empty(), 32), + ], + ); + b.set_crc(bundle_protocol::crc::CRC_32); + b.sort_canonicals(); + b +} + +pub fn fragments(from: &str, to: &str, payload: &[u8], cuts: &[usize]) -> Vec { + let created = CreationTimestamp::now(); + let mut bounds = vec![0]; + bounds.extend_from_slice(cuts); + bounds.push(payload.len()); + bounds + .windows(2) + .map(|w| { + let primary = PrimaryBlockBuilder::default() + .bundle_control_flags(BundleControlFlags::BUNDLE_IS_FRAGMENT.bits()) + .destination(eid(to)) + .source(eid(from)) + .report_to(EndpointID::none()) + .creation_timestamp(created.clone()) + .lifetime(Duration::from_secs(3600)) + .fragmentation_offset(w[0] as u64) + .total_data_length(payload.len() as u64) + .build() + .unwrap(); + let mut b = Bundle::new( + primary, + vec![new_payload_block(BlockControlFlags::empty(), payload[w[0]..w[1]].to_vec())], + ); + b.set_crc(bundle_protocol::crc::CRC_32); + b + }) + .collect() +} + +pub fn hum_block(b: &Bundle) -> Option { + b.canonicals.iter().find(|c| c.block_type == HUM_BLOCK).and_then(|c| match c.data() { + CanonicalData::Unknown(bytes) => serde_json::from_slice(bytes).ok(), + _ => None, + }) +} + +pub fn with_hum_block(mut b: Bundle, body: Value) -> Bundle { + let block = CanonicalBlockBuilder::default() + .block_type(HUM_BLOCK) + .block_number(3) + .block_control_flags(BlockControlFlags::BLOCK_REPLICATE.bits()) + .data(CanonicalData::Unknown(serde_json::to_vec(&body).unwrap())) + .build() + .unwrap(); + b.canonicals.push(block); + b.set_crc(bundle_protocol::crc::CRC_32); + b.sort_canonicals(); + b +} + +pub fn dtn_ms_ago(ms: u64) -> CreationTimestamp { + CreationTimestamp::with_time_and_seq(dtn_time_now() - ms, 0) +} + +pub struct Reply { + pub text: String, + pub bundles: Vec, + pub sizes: Vec, +} + +impl Reply { + pub fn first(&self) -> &Bundle { + &self.bundles[0] + } +} + +pub struct Node { + pub sock: UdpSocket, + pub eid: String, +} + +impl Node { + pub async fn new(name: &str) -> Node { + Node { sock: UdpSocket::bind("127.0.0.1:0").await.unwrap(), eid: format!("dtn://{name}.test/") } + } + + pub fn addr(&self) -> SocketAddr { + self.sock.local_addr().unwrap() + } + + pub async fn send(&self, b: &mut Bundle, to: SocketAddr) { + self.sock.send_to(&b.to_cbor(), to).await.unwrap(); + } + + pub async fn send_raw(&self, bytes: &[u8], to: SocketAddr) { + self.sock.send_to(bytes, to).await.unwrap(); + } + + pub async fn replies(&self, n: usize, within: Duration) -> Vec { + let deadline = tokio::time::Instant::now() + within; + let mut done = vec![]; + let mut partial: HashMap> = HashMap::new(); + let mut buf = vec![0u8; 65536]; + while done.len() < n { + let Ok(Ok((len, _))) = tokio::time::timeout_at(deadline, self.sock.recv_from(&mut buf)).await else { + break; + }; + let b = Bundle::try_from(&buf[..len]).expect("the forager sends bundles"); + if !b.primary.has_fragmentation() { + let text = String::from_utf8_lossy(b.payload().map(|p| p.as_slice()).unwrap_or(&[])).into_owned(); + done.push(Reply { text, bundles: vec![b], sizes: vec![len] }); + continue; + } + let key = format!("{}-{}", b.primary.source, b.primary.creation_timestamp); + let total = b.primary.total_data_length as usize; + let parts = partial.entry(key.clone()).or_default(); + parts.push((b, len)); + let mut covered = vec![false; total]; + for (p, _) in parts.iter() { + let off = p.primary.fragmentation_offset as usize; + let n = p.payload().map(|d| d.len()).unwrap_or(0); + covered[off..off + n].iter_mut().for_each(|c| *c = true); + } + if covered.iter().all(|c| *c) { + let mut parts = partial.remove(&key).unwrap(); + parts.sort_by_key(|(p, _)| p.primary.fragmentation_offset); + let mut adu = vec![0u8; total]; + for (p, _) in &parts { + let off = p.primary.fragmentation_offset as usize; + let d = p.payload().unwrap(); + adu[off..off + d.len()].copy_from_slice(d); + } + let sizes = parts.iter().map(|(_, s)| *s).collect(); + let bundles = parts.into_iter().map(|(b, _)| b).collect(); + done.push(Reply { text: String::from_utf8_lossy(&adu).into_owned(), bundles, sizes }); + } + } + done + } + + pub async fn reply(&self, within: Duration) -> Option { + self.replies(1, within).await.pop() + } +} + +pub fn spool_files(dir: &Path) -> usize { + fn walk(d: &Path) -> usize { + std::fs::read_dir(d) + .map(|rd| { + rd.filter_map(Result::ok) + .map(|e| if e.path().is_dir() { walk(&e.path()) } else { 1 }) + .sum() + }) + .unwrap_or(0) + } + walk(dir) +} diff --git a/hives/bp7/tests/common/near.rs b/hives/bp7/tests/common/near.rs new file mode 100644 index 00000000..373cedcb --- /dev/null +++ b/hives/bp7/tests/common/near.rs @@ -0,0 +1,97 @@ +//! The ensemble around the forager: the humd in the test's own process is +//! the far one, with the nurse; the near one, where the forager lives and +//! no nurse does, runs as its own process under roots of its own and +//! reaches the far one over TCP. + +use std::path::{Path, PathBuf}; +use std::process::Stdio; +use std::time::{Duration, Instant}; + +use serde_json::json; + +use super::{world, Forager, WAIT}; + +const ROOTS: [(&str, &str); 5] = [ + ("XDG_STATE_HOME", "s"), + ("XDG_CONFIG_HOME", "c"), + ("XDG_DATA_HOME", "d"), + ("XDG_CACHE_HOME", "k"), + ("XDG_RUNTIME_DIR", "r"), +]; + +pub struct Near { + _child: tokio::process::Child, + root: PathBuf, + log: PathBuf, +} + +impl Near { + pub fn env(&self) -> Vec<(String, String)> { + ROOTS.iter().map(|(k, v)| (k.to_string(), self.root.join(v).display().to_string())).collect() + } + + pub fn log_text(&self) -> String { + std::fs::read_to_string(&self.log).unwrap_or_default() + } +} + +fn under(root: &Path, p: PathBuf) -> PathBuf { + root.join(p.strip_prefix(&world().root).expect("a hum path is under the world's root")) +} + +fn humd_bin() -> PathBuf { + let exe = std::env::current_exe().unwrap(); + let path = exe.parent().unwrap().parent().unwrap().join("examples").join("humd"); + assert!(path.exists(), "build it first: cargo build --example humd ({})", path.display()); + path +} + +pub fn far_reach() -> (String, String) { + let since = Instant::now(); + loop { + if let Some(info) = hum_paths::RuntimeInfo::read() + && let Some(tcp) = info.ensemble_addrs.iter().find(|a| a.starts_with("tcp:")) + { + return (humd::load_or_mint_key().unwrap().hid().to_hex(), tcp.clone()); + } + assert!(since.elapsed() < WAIT, "the far humd published no tcp address"); + std::thread::sleep(Duration::from_millis(50)); + } +} + +pub async fn spawn_near(far_hid: &str, far_tcp: &str) -> Near { + let root = world().root.join("near"); + let at = |p: PathBuf| under(&root, p); + std::fs::create_dir_all(at(hum_paths::hives_d_dir())).unwrap(); + let humd_cfg = json!({"humd": {"metricsAddr": "none", "irohPort": 0, "trustRemoteNurses": true}}); + std::fs::write(at(hum_paths::hum_json()), humd_cfg.to_string()).unwrap(); + let peers = json!({"peers": [{"humd_id": far_hid, "hints": [far_tcp]}]}); + std::fs::write(at(hum_paths::peers_json()), peers.to_string()).unwrap(); + for hive in ["bp7", "nurse-echo"] { + std::fs::write(at(hum_paths::hive_installation(hive)), "# the near humd recognises it\n").unwrap(); + } + let log = world().root.join("near-humd.log"); + let mut cmd = tokio::process::Command::new(humd_bin()); + for (k, v) in ROOTS { + cmd.env(k, root.join(v)); + } + cmd.env("HUM_LOG_LEVEL", "info") + .env_remove("HUM_THRUM_SOCK") + .stdout(std::fs::File::create(&log).unwrap()) + .stderr(Stdio::null()) + .kill_on_drop(true); + let near = Near { _child: cmd.spawn().unwrap(), root, log }; + let sock = under(&near.root, hum_paths::thrum_sock_resolved()); + let since = Instant::now(); + while std::os::unix::net::UnixStream::connect(&sock).is_err() { + assert!(since.elapsed() < WAIT, "the near humd did not bind {}:\n{}", sock.display(), near.log_text()); + tokio::time::sleep(Duration::from_millis(50)).await; + } + near +} + +pub async fn forager_on(near: &Near, name: &str) -> Forager { + let env = near.env(); + let env: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect(); + Forager::start(name, &env).await +} diff --git a/hives/bp7/tests/dtn.rs b/hives/bp7/tests/dtn.rs new file mode 100644 index 00000000..09955473 --- /dev/null +++ b/hives/bp7/tests/dtn.rs @@ -0,0 +1,304 @@ +//! The forager as a DTN node meets it: bundles that arrive twice, late, +//! in pieces, corrupted, all at once, or that are not asks at all, and +//! replies that must find their way home through a router. + +mod common; + +use std::time::Duration; + +use bundle_protocol::administrative_record::{new_status_report_bundle, DELIVERED_BUNDLE, NO_INFORMATION}; +use bundle_protocol::flags::BundleControlFlags; +use common::*; +use serde_json::json; + +#[tokio::test(flavor = "multi_thread")] +async fn a_bundle_is_answered() { + let f = Forager::start("plain", &[]).await; + let node = Node::new("plain").await; + node.send(&mut ask(&node.eid, &f.eid, b"hello from the dark"), f.addr).await; + let reply = node.reply(WAIT).await.expect("an answer comes home"); + assert_eq!(reply.text, "echo: hello from the dark"); +} + +#[tokio::test(flavor = "multi_thread")] +async fn a_json_ask_names_its_model_and_system() { + let f = Forager::start("json", &[]).await; + let node = Node::new("json").await; + let body = json!({"text": "json-ask", "modelId": MODEL, "system": "be brief"}); + node.send(&mut ask(&node.eid, &f.eid, body.to_string().as_bytes()), f.addr).await; + let reply = node.reply(WAIT).await.expect("an answer comes home"); + assert_eq!(reply.text, "echo: json-ask"); +} + +#[tokio::test(flavor = "multi_thread")] +async fn a_reply_goes_to_the_route_not_to_the_send_port() { + let router = Node::new("router").await; + let route = router.addr().to_string(); + let f = Forager::start("routed", &[("BP7_ROUTE", &route)]).await; + let sender = Node::new("sender").await; + sender.send(&mut ask(&sender.eid, &f.eid, b"route-me"), f.addr).await; + let reply = router.reply(WAIT).await.expect("the reply reaches the router"); + assert_eq!(reply.text, "echo: route-me"); + assert_eq!(reply.first().primary.destination.to_string(), sender.eid, "addressed to the asker, through the router"); + assert!(sender.reply(QUIET).await.is_none(), "nothing goes back to the ephemeral port it was sent from"); +} + +#[tokio::test(flavor = "multi_thread")] +async fn a_bundle_heard_twice_is_answered_once() { + let f = Forager::start("twice", &[]).await; + let node = Node::new("twice").await; + let mut b = ask(&node.eid, &f.eid, b"twice-marker"); + let bytes = b.to_cbor(); + node.send_raw(&bytes, f.addr).await; + node.send_raw(&bytes, f.addr).await; + let replies = node.replies(2, WAIT.min(Duration::from_secs(5))).await; + assert_eq!(replies.len(), 1, "one answer for one bundle"); + assert_eq!(world().heard("twice-marker").len(), 1, "one bloom for one bundle"); + node.send_raw(&bytes, f.addr).await; + assert!(node.reply(QUIET).await.is_none(), "a duplicate after the answer is not answered again"); + assert_eq!(world().heard("twice-marker").len(), 1); +} + +#[tokio::test(flavor = "multi_thread")] +async fn a_burst_is_answered_each_to_its_own() { + let f = Forager::start("burst", &[]).await; + let node = Node::new("burst").await; + let n = 16; + for i in 0..n { + node.send(&mut ask(&node.eid, &f.eid, format!("burst-{i}").as_bytes()), f.addr).await; + } + let replies = node.replies(n, WAIT).await; + let mut got: Vec = replies.iter().map(|r| r.text.clone()).collect(); + got.sort(); + let mut want: Vec = (0..n).map(|i| format!("echo: burst-{i}")).collect(); + want.sort(); + assert_eq!(got, want, "every ask gets its own answer, and only its own"); + for r in &replies { + let asked = r.text.trim_start_matches("echo: "); + let block = hum_block(r.first()).expect("the reply says what it answers"); + assert!(block["to"].as_str().is_some_and(|id| !id.is_empty()), "{block}"); + let bloomed = world().heard(asked).iter().filter(|h| h.text == asked).count(); + assert_eq!(bloomed, 1, "{asked} bloomed once"); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn blooms_are_bounded() { + let f = Forager::start("bounded", &[("BP7_BLOOMS", "2")]).await; + let node = Node::new("bounded").await; + for i in 0..6 { + node.send(&mut ask(&node.eid, &f.eid, format!("slow 300 bounded-{i}").as_bytes()), f.addr).await; + } + let replies = node.replies(6, WAIT).await; + assert_eq!(replies.len(), 6); + let peak = world().peak("bounded"); + assert!((1..=2).contains(&peak), "at most BP7_BLOOMS blooms at once, saw {peak}"); + assert!( + world().heard("bounded-").len() == 6, + "every ask bloomed once: {:?}", + world().heard("bounded-") + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn an_expired_bundle_is_not_answered() { + let f = Forager::start("expired", &[]).await; + let node = Node::new("expired").await; + let mut old = ask_with(&node.eid, &f.eid, b"expired-marker", Duration::from_secs(60), dtn_ms_ago(3_600_000)); + node.send(&mut old, f.addr).await; + node.send(&mut ask(&node.eid, &f.eid, b"expired-sentinel"), f.addr).await; + let reply = node.reply(WAIT).await.expect("the live ask is answered"); + assert_eq!(reply.text, "echo: expired-sentinel"); + assert!(node.reply(QUIET).await.is_none()); + assert!(world().heard("expired-marker").is_empty(), "a bundle past its lifetime is not bloomed"); +} + +#[tokio::test(flavor = "multi_thread")] +async fn a_status_report_is_not_an_ask() { + let f = Forager::start("report", &[]).await; + let node = Node::new("report").await; + let mut ours = ask(&f.eid, &node.eid, b"report-marker"); + ours.primary.report_to = eid(&f.eid); + let mut report = new_status_report_bundle( + &ours, + eid(&node.eid), + bundle_protocol::crc::CRC_32, + DELIVERED_BUNDLE, + NO_INFORMATION, + ); + node.send(&mut report, f.addr).await; + node.send(&mut ask(&node.eid, &f.eid, b"report-sentinel"), f.addr).await; + assert_eq!(node.reply(WAIT).await.expect("the ask is answered").text, "echo: report-sentinel"); + assert!(node.reply(QUIET).await.is_none(), "a status report is never answered"); + assert_eq!(world().heard("report-sentinel").len(), 1); + assert!( + world().heard("").iter().all(|h| !h.text.contains('\u{fffd}')), + "no administrative record reached a nurse as words" + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn a_reply_is_never_answered() { + let f = Forager::start("loop", &[]).await; + let node = Node::new("loop").await; + let mut reply = with_hum_block( + ask(&node.eid, &f.eid, b"loop-marker: an answer from another hum"), + json!({"hum": "reply", "to": "dtn://elsewhere/-1-0", "ok": true}), + ); + node.send(&mut reply, f.addr).await; + node.send(&mut ask(&node.eid, &f.eid, b"loop-sentinel"), f.addr).await; + assert_eq!(node.reply(WAIT).await.expect("the ask is answered").text, "echo: loop-sentinel"); + assert!(node.reply(QUIET).await.is_none(), "two hums never answer each other's answers"); + assert!(world().heard("loop-marker").is_empty()); +} + +#[tokio::test(flavor = "multi_thread")] +async fn the_reply_is_a_bundle_any_dtn_node_accepts() { + let f = Forager::start("conform", &[]).await; + let node = Node::new("conform").await; + let mut asked = ask_with(&node.eid, &f.eid, b"conform-marker", Duration::from_secs(86_400), bundle_protocol::CreationTimestamp::now()); + let asked_id = asked.id(); + node.send(&mut asked, f.addr).await; + let reply = node.reply(WAIT).await.expect("an answer comes home"); + let mut b = reply.first().clone(); + assert!(b.validate().is_ok(), "{:?}", b.validate()); + assert!(b.primary.crc.has_crc(), "RFC 9171 4.3.1: a primary block without a BIB carries a CRC"); + assert!(b.crc_valid()); + assert_eq!(b.primary.source.to_string(), f.eid); + assert_eq!(b.primary.destination.to_string(), node.eid); + assert!(b.primary.lifetime >= Duration::from_secs(86_400), "the answer lives as long as the ask was given"); + let flags = b.primary.bundle_control_flags; + for asked_for in [ + BundleControlFlags::BUNDLE_STATUS_REQUEST_RECEPTION, + BundleControlFlags::BUNDLE_STATUS_REQUEST_FORWARD, + BundleControlFlags::BUNDLE_STATUS_REQUEST_DELIVERY, + BundleControlFlags::BUNDLE_STATUS_REQUEST_DELETION, + ] { + assert_eq!(flags & asked_for.bits(), 0, "a reply asks for no reports, so none can loop back as an ask"); + } + assert!(!b.is_administrative_record()); + let block = hum_block(&b).expect("the reply is marked as hum's answer"); + assert_eq!(block["hum"], "reply"); + assert_eq!(block["to"], asked_id); + assert_eq!(block["ok"], true); +} + +#[tokio::test(flavor = "multi_thread")] +async fn a_long_answer_is_fragmented_to_the_mtu() { + let f = Forager::start("long", &[("BP7_MTU", "1200")]).await; + let node = Node::new("long").await; + node.send(&mut ask(&node.eid, &f.eid, b"big 100000"), f.addr).await; + let reply = node.reply(WAIT).await.expect("a long answer still comes home"); + assert_eq!(reply.text.len(), 100_000); + assert!(reply.text.bytes().all(|c| c == b'x')); + assert!(reply.bundles.len() > 1, "fragmented"); + assert!(reply.sizes.iter().all(|s| *s <= 1200), "every datagram fits: {:?}", reply.sizes); + for b in &reply.bundles { + let mut b = b.clone(); + assert!(b.crc_valid() && b.validate().is_ok()); + assert!(hum_block(&b).is_some(), "the mark rides in every fragment"); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn a_fragmented_ask_is_reassembled() { + let f = Forager::start("pieces", &[]).await; + let node = Node::new("pieces").await; + let text = "pieces-marker: ".to_string() + &"a question long enough to be cut ".repeat(20); + let mut frags = fragments(&node.eid, &f.eid, text.as_bytes(), &[100, 400]); + for i in [2, 0, 0, 1] { + node.send(&mut frags[i], f.addr).await; + } + let reply = node.reply(WAIT).await.expect("the whole ask is answered"); + assert_eq!(reply.text, format!("echo: {text}")); + let heard = world().heard("pieces-marker"); + assert_eq!(heard.len(), 1, "one bloom for the whole ask, not one per piece"); + assert_eq!(heard[0].text, text); +} + +#[tokio::test(flavor = "multi_thread")] +async fn a_corrupt_bundle_is_dropped() { + let f = Forager::start("corrupt", &[]).await; + let node = Node::new("corrupt").await; + let mut bytes = ask(&node.eid, &f.eid, b"corrupt-marker: intact").to_cbor(); + let at = bytes.windows(6).position(|w| w == b"intact").unwrap(); + bytes[at] = b'I'; + node.send_raw(&bytes, f.addr).await; + node.send(&mut ask(&node.eid, &f.eid, b"corrupt-sentinel"), f.addr).await; + assert_eq!(node.reply(WAIT).await.expect("the clean ask is answered").text, "echo: corrupt-sentinel"); + assert!(node.reply(QUIET).await.is_none()); + assert!(world().heard("corrupt-marker").is_empty(), "a bundle whose CRC fails is not bloomed"); +} + +#[tokio::test(flavor = "multi_thread")] +async fn an_anonymous_bundle_is_not_bloomed() { + let f = Forager::start("anon", &[]).await; + let node = Node::new("anon").await; + node.send(&mut ask("dtn:none", &f.eid, b"anon-marker"), f.addr).await; + node.send(&mut ask(&node.eid, &f.eid, b"anon-sentinel"), f.addr).await; + assert_eq!(node.reply(WAIT).await.expect("the signed-off ask is answered").text, "echo: anon-sentinel"); + assert!(world().heard("anon-marker").is_empty(), "no one could hear the answer, so no bloom grows it"); +} + +#[tokio::test(flavor = "multi_thread")] +async fn a_bundle_for_another_endpoint_is_not_ours() { + let f = Forager::start("other", &[]).await; + let node = Node::new("other").await; + let near = format!("{}-evil", f.eid); + node.send(&mut ask(&node.eid, &near, b"other-marker"), f.addr).await; + node.send(&mut ask(&node.eid, &f.eid, b"other-sentinel"), f.addr).await; + assert_eq!(node.reply(WAIT).await.expect("ours is answered").text, "echo: other-sentinel"); + assert!(world().heard("other-marker").is_empty(), "{near} is not {}", f.eid); +} + +#[tokio::test(flavor = "multi_thread")] +async fn a_broken_bloom_is_answered_with_what_broke() { + let f = Forager::start("broken", &[]).await; + let node = Node::new("broken").await; + node.send(&mut ask(&node.eid, &f.eid, b"fail"), f.addr).await; + let reply = node.reply(WAIT).await.expect("the asker hears that it broke"); + assert!(reply.text.contains("the cell broke"), "{}", reply.text); + let block = hum_block(reply.first()).expect("marked"); + assert_eq!(block["ok"], false); +} + +#[tokio::test(flavor = "multi_thread")] +async fn an_ask_waits_for_a_nurse_to_come() { + let f = Forager::start("later", &[]).await; + let node = Node::new("later").await; + let body = json!({"text": "later-marker", "modelId": "later"}); + node.send(&mut ask(&node.eid, &f.eid, body.to_string().as_bytes()), f.addr).await; + assert!(node.reply(Duration::from_secs(2)).await.is_none(), "no nurse grows `later` yet"); + world().nurse("nurse-later", "later"); + let reply = node.reply(WAIT).await.expect("answered once a nurse comes"); + assert_eq!(reply.text, "echo: later-marker"); +} + +#[tokio::test(flavor = "multi_thread")] +async fn an_ask_outlives_the_forager() { + let f = Forager::start("phoenix", &[]).await; + let node = Node::new("phoenix").await; + node.send(&mut ask(&node.eid, &f.eid, b"slow 2000 phoenix-marker"), f.addr).await; + let since = std::time::Instant::now(); + while world().heard("phoenix-marker").is_empty() { + assert!(since.elapsed() < WAIT, "the nurse never heard it"); + tokio::time::sleep(Duration::from_millis(20)).await; + } + let f = f.restart().await; + let reply = node.reply(WAIT).await.expect("the ask survived the forager's restart"); + assert_eq!(reply.text, "echo: phoenix-marker"); + assert!(node.reply(Duration::from_secs(3)).await.is_none(), "and is answered once"); + drop(f); +} + +#[tokio::test(flavor = "multi_thread")] +async fn the_spool_forgets_what_has_expired() { + let f = Forager::start("forget", &[("BP7_SWEEP_MS", "200")]).await; + let node = Node::new("forget").await; + let mut b = ask_with(&node.eid, &f.eid, b"forget-marker", Duration::from_secs(2), bundle_protocol::CreationTimestamp::now()); + node.send(&mut b, f.addr).await; + assert_eq!(node.reply(WAIT).await.expect("answered").text, "echo: forget-marker"); + assert!(spool_files(&f.spool) > 0, "the answer is remembered while a duplicate could still arrive"); + tokio::time::sleep(Duration::from_secs(3)).await; + assert_eq!(spool_files(&f.spool), 0, "and forgotten once no duplicate can"); +} diff --git a/hives/bp7/tests/ensemble.rs b/hives/bp7/tests/ensemble.rs new file mode 100644 index 00000000..699b3c14 --- /dev/null +++ b/hives/bp7/tests/ensemble.rs @@ -0,0 +1,33 @@ +//! Space reaching into the ensemble. A DTN node's bundle lands on a +//! forager whose humd has no nurse at all; the only nurse that grows the +//! model is on another humd, reached over the ensemble, and the ask is +//! answered from there. + +mod common; + +use std::time::Duration; + +use common::near::{far_reach, forager_on, spawn_near}; +use common::*; + +#[tokio::test(flavor = "multi_thread")] +async fn space_reaches_into_the_ensemble() { + let w = ensemble_world(); + let (far_hid, far_tcp) = far_reach(); + let near = spawn_near(&far_hid, &far_tcp).await; + let f = forager_on(&near, "reach").await; + let node = Node::new("mars").await; + + for i in 0..3 { + let words = format!("reach-marker-{i}: what is the speed of light in glass"); + node.send(&mut ask(&node.eid, &f.eid, words.as_bytes()), f.addr).await; + let reply = node.reply(WAIT).await.unwrap_or_else(|| { + panic!("no answer from the ensemble\n--- forager\n{}\n--- near humd\n{}", f.log_text(), near.log_text()) + }); + assert_eq!(reply.text, format!("echo: {words}")); + let heard = w.heard(&format!("reach-marker-{i}:")); + assert_eq!(heard.len(), 1, "grown once, by the far humd's nurse"); + assert!(heard[0].sid.starts_with("bp7-"), "{:?}", heard[0]); + } + assert!(node.reply(Duration::from_secs(1)).await.is_none()); +} diff --git a/hives/bp7/tests/grower_gone.rs b/hives/bp7/tests/grower_gone.rs new file mode 100644 index 00000000..d72d6c60 --- /dev/null +++ b/hives/bp7/tests/grower_gone.rs @@ -0,0 +1,41 @@ +//! The humd growing an ask goes away mid-bloom. The ask should be grown +//! again, once that humd is back or by another; today it waits out its +//! lifetime instead, because the near humd never tells the forager that +//! the peer it handed the bloom to is gone. See "What is not yet" in the +//! README. + +mod common; + +use std::time::{Duration, Instant}; + +use common::near::{far_reach, forager_on, spawn_near}; +use common::*; + +#[tokio::test(flavor = "multi_thread")] +#[ignore = "humd does not yet break off a bloom whose grower left the ensemble; see README, What is not yet"] +async fn an_ask_outlives_the_humd_growing_it() { + let w = ensemble_world(); + let (far_hid, far_tcp) = far_reach(); + w.pin_tcp(&far_tcp); + let near = spawn_near(&far_hid, &far_tcp).await; + let f = forager_on(&near, "gone").await; + let node = Node::new("gone").await; + + node.send(&mut ask(&node.eid, &f.eid, b"slow 3000 gone-marker"), f.addr).await; + let since = Instant::now(); + while w.heard("gone-marker").is_empty() { + assert!(since.elapsed() < WAIT, "the far nurse never heard it\n{}", f.log_text()); + tokio::time::sleep(Duration::from_millis(20)).await; + } + w.stop_humd(); + tokio::time::sleep(Duration::from_millis(500)).await; + w.start_humd(); + let reply = node.reply(Duration::from_secs(90)).await.unwrap_or_else(|| { + panic!( + "the ask hung when the humd growing it went away\n--- forager\n{}\n--- near humd\n{}", + f.log_text(), + near.log_text() + ) + }); + assert_eq!(reply.text, "echo: gone-marker"); +} diff --git a/hives/bp7/tests/outlive.rs b/hives/bp7/tests/outlive.rs new file mode 100644 index 00000000..f2181de6 --- /dev/null +++ b/hives/bp7/tests/outlive.rs @@ -0,0 +1,32 @@ +//! The forager across humd's absence. A DTN node does not know or care +//! that the humd behind the forager restarted: an ask the forager took +//! is answered when humd is back, whether it arrived while humd was gone +//! or was blooming when humd went. + +mod common; + +use std::time::Duration; + +use common::*; + +#[tokio::test(flavor = "multi_thread")] +async fn asks_outlive_humd() { + let f = Forager::start("outlive", &[]).await; + let node = Node::new("outlive").await; + + node.send(&mut ask(&node.eid, &f.eid, b"slow 1500 mid-bloom-marker"), f.addr).await; + let since = std::time::Instant::now(); + while world().heard("mid-bloom-marker").is_empty() { + assert!(since.elapsed() < WAIT, "the nurse never heard it"); + tokio::time::sleep(Duration::from_millis(20)).await; + } + world().stop_humd(); + node.send(&mut ask(&node.eid, &f.eid, b"while-away-marker"), f.addr).await; + assert!(node.reply(Duration::from_secs(2)).await.is_none(), "nothing can bloom while humd is away"); + world().start_humd(); + + let mut got: Vec = node.replies(2, WAIT).await.into_iter().map(|r| r.text).collect(); + got.sort(); + assert_eq!(got, ["echo: mid-bloom-marker", "echo: while-away-marker"], "{}", f.log_text()); + assert!(node.reply(Duration::from_secs(3)).await.is_none(), "each answered once"); +}