diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8085a40..b6f1309 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,4 +36,4 @@ jobs: cargo clippy --all-targets -- -D warnings - name: Test - run: cargo test --locked + run: cargo test diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 72087dd..b1d7271 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,7 +8,6 @@ on: branches: [master] paths: - Cargo.toml - - Cargo.lock - crates/** workflow_dispatch: inputs: diff --git a/.gitignore b/.gitignore index 66cc710..e13de17 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target/ +Cargo.lock diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index 1274e91..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,4142 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "agency-proxy" -version = "0.1.10" -dependencies = [ - "agency-proxy-client", - "agency-proxy-protocol", - "agent-abstraction", - "async-trait", - "clap", - "endpoint-libs", - "eyre", - "futures", - "libc", - "serde", - "serde_json", - "tempfile", - "thiserror", - "tokio", - "tokio-tungstenite", -] - -[[package]] -name = "agency-proxy-client" -version = "0.1.10" -dependencies = [ - "agency-proxy-protocol", - "endpoint-libs", - "futures", - "serde_json", - "thiserror", - "tokio", -] - -[[package]] -name = "agency-proxy-protocol" -version = "0.1.10" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "agent-abstraction" -version = "0.4.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ddf92d92000d84579f3344b73d62f04a4bc0ff32f90ff4ee69a8ff243f595c4" -dependencies = [ - "fs2", - "libc", - "serde", - "serde_json", - "thiserror", - "tokio", - "uuid", -] - -[[package]] -name = "aho-corasick" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloy-primitives" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f007e257069855bdf21d27762fd3f3705a613f805c9a08309bf353503f081d71" -dependencies = [ - "alloy-rlp", - "bytes", - "cfg-if", - "const-hex", - "derive_more", - "fixed-cache", - "foldhash", - "indexmap 2.14.0", - "itoa", - "k256", - "keccak-asm", - "paste", - "proptest", - "rand 0.9.5", - "rapidhash", - "ruint", - "rustc-hash", - "secp256k1", - "serde", - "sha3", -] - -[[package]] -name = "alloy-rlp" -version = "0.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24671b1f62edcf0f9b62994c7bf72cd621a04a4b99f5020ece1a647b40e2f103" -dependencies = [ - "arrayvec", - "bytes", -] - -[[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 = "anstream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "anyhow" -version = "1.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" - -[[package]] -name = "ark-ff" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6" -dependencies = [ - "ark-ff-asm 0.3.0", - "ark-ff-macros 0.3.0", - "ark-serialize 0.3.0", - "ark-std 0.3.0", - "derivative", - "num-bigint", - "num-traits", - "paste", - "rustc_version 0.3.3", - "zeroize", -] - -[[package]] -name = "ark-ff" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" -dependencies = [ - "ark-ff-asm 0.4.2", - "ark-ff-macros 0.4.2", - "ark-serialize 0.4.2", - "ark-std 0.4.0", - "derivative", - "digest 0.10.7", - "itertools 0.10.5", - "num-bigint", - "num-traits", - "paste", - "rustc_version 0.4.1", - "zeroize", -] - -[[package]] -name = "ark-ff" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" -dependencies = [ - "ark-ff-asm 0.5.0", - "ark-ff-macros 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", - "arrayvec", - "digest 0.10.7", - "educe", - "itertools 0.13.0", - "num-bigint", - "num-traits", - "paste", - "zeroize", -] - -[[package]] -name = "ark-ff" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7a806ac6c8307b929df4645776290a50ee2aac754ad09d8bdf73391309e43af" -dependencies = [ - "ark-ff-asm 0.6.0", - "ark-ff-macros 0.6.0", - "ark-serialize 0.6.0", - "ark-std 0.6.0", - "digest 0.10.7", - "educe", - "num-bigint", - "num-traits", - "zeroize", -] - -[[package]] -name = "ark-ff-asm" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "ark-ff-asm" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "ark-ff-asm" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" -dependencies = [ - "quote", - "syn 2.0.119", -] - -[[package]] -name = "ark-ff-asm" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1479009684adc073dff49a1025d3a7065b317a9ead25aaaca38cdc70058ba8a2" -dependencies = [ - "quote", - "syn 2.0.119", -] - -[[package]] -name = "ark-ff-macros" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" -dependencies = [ - "num-bigint", - "num-traits", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "ark-ff-macros" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" -dependencies = [ - "num-bigint", - "num-traits", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "ark-ff-macros" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" -dependencies = [ - "num-bigint", - "num-traits", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "ark-ff-macros" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0691ed21ef00ef89c1e9bda832eba493dda3ec2f8d892fb25b705f73f06bb8" -dependencies = [ - "num-bigint", - "num-traits", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "ark-serialize" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6c2b318ee6e10f8c2853e73a83adc0ccb88995aa978d8a3408d492ab2ee671" -dependencies = [ - "ark-std 0.3.0", - "digest 0.9.0", -] - -[[package]] -name = "ark-serialize" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" -dependencies = [ - "ark-std 0.4.0", - "digest 0.10.7", - "num-bigint", -] - -[[package]] -name = "ark-serialize" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" -dependencies = [ - "ark-std 0.5.0", - "arrayvec", - "digest 0.10.7", - "num-bigint", -] - -[[package]] -name = "ark-serialize" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a74dd304fd536fb95d0a328e72be759209cc496a9da094c5bc56e5fea4f9e86b" -dependencies = [ - "ark-serialize-derive", - "ark-std 0.6.0", - "digest 0.10.7", - "num-bigint", - "serde_with", -] - -[[package]] -name = "ark-serialize-derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f153690697a2b91e5e1251ff98411ee5371500a111a0fd317a70e588eb300f9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "ark-std" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" -dependencies = [ - "num-traits", - "rand 0.8.7", -] - -[[package]] -name = "ark-std" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" -dependencies = [ - "num-traits", - "rand 0.8.7", -] - -[[package]] -name = "ark-std" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" -dependencies = [ - "num-traits", - "rand 0.8.7", -] - -[[package]] -name = "ark-std" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "367c9c827ed431bff6868b7aa926e05b16eb46603cc8b6e768e4a5553fa1d155" -dependencies = [ - "num-traits", - "rand 0.8.7", -] - -[[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.3", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "auto_impl" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "axum" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" -dependencies = [ - "axum-core", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "serde_core", - "sync_wrapper", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-core" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "sync_wrapper", - "tower-layer", - "tower-service", -] - -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64ct" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" - -[[package]] -name = "bitcoin-consensus-encoding" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "207311705279250ba465076a1bac4b1ac982855fff73fc5f67e22158ac58cdc9" -dependencies = [ - "bitcoin-internals", - "hex-conservative 1.2.0", - "serde", -] - -[[package]] -name = "bitcoin-internals" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d573f4cf32996a8dce612e4348cece65a241f1882ed594047c9ba348e8869fa5" - -[[package]] -name = "bitcoin-io" -version = "0.1.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb5de036369d1ac59d3c1819ebc4d850f89466f5401c571a285b6ed564a4cb78" -dependencies = [ - "bitcoin-consensus-encoding", -] - -[[package]] -name = "bitcoin_hashes" -version = "0.14.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" -dependencies = [ - "bitcoin-io", - "hex-conservative 0.2.2", -] - -[[package]] -name = "bitflags" -version = "2.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" - -[[package]] -name = "bitvec" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block-buffer" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" -dependencies = [ - "hybrid-array", -] - -[[package]] -name = "bs58" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "byte-slice-cast" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" - -[[package]] -name = "cc" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "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.0", - "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 = "clap" -version = "4.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - -[[package]] -name = "const-hex" -version = "1.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e2a781ebdf4467d1428dc4593067825fb646f6871475098d8577421af73558" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "proptest", - "serde_core", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "const_format" -version = "0.2.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" -dependencies = [ - "const_format_proc_macros", - "konst", -] - -[[package]] -name = "const_format_proc_macros" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "convert_case" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] - -[[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 = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "cpufeatures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -dependencies = [ - "libc", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" - -[[package]] -name = "crossfire" -version = "3.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111ce8f7abfbac38b4bc4f32a3a2dda1a8034b873c90c7899f302cc9dbbc05ec" -dependencies = [ - "crossbeam-utils", - "futures-core", - "parking_lot", - "pointers", - "smallvec", - "tokio", -] - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "crypto-common" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" -dependencies = [ - "hybrid-array", -] - -[[package]] -name = "dashmap" -version = "6.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - -[[package]] -name = "data-encoding" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "serde_core", -] - -[[package]] -name = "derivative" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[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 0.10.0", - "proc-macro2", - "quote", - "rustc_version 0.4.1", - "syn 2.0.119", - "unicode-xid", -] - -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - -[[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", - "const-oid", - "crypto-common 0.1.6", - "subtle", -] - -[[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 = "displaydoc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "ecdsa" -version = "0.16.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" -dependencies = [ - "der", - "digest 0.10.7", - "elliptic-curve", - "rfc6979", - "signature", - "spki", -] - -[[package]] -name = "educe" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" -dependencies = [ - "enum-ordinalize", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "either" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" - -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct", - "crypto-bigint", - "digest 0.10.7", - "ff", - "generic-array", - "group", - "pkcs8", - "rand_core 0.6.4", - "sec1", - "subtle", - "zeroize", -] - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "endpoint-libs" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64708d7972a5c06112029ef04599061238abc55ba5e879a0e02dfc373d24df34" -dependencies = [ - "alloy-primitives", - "async-trait", - "bytes", - "chrono", - "clap", - "convert_case 0.6.0", - "crossfire", - "dashmap", - "derive_more", - "eyre", - "futures", - "hex", - "http-body-util", - "httpdate", - "hyper", - "hyper-rustls", - "hyper-util", - "itertools 0.12.1", - "lazy_static", - "opentelemetry", - "opentelemetry-appender-tracing", - "opentelemetry-otlp", - "opentelemetry-semantic-conventions", - "opentelemetry_sdk", - "parking_lot", - "reqwest", - "rustls", - "serde", - "serde_json", - "serde_path_to_error", - "tokio", - "tokio-rustls", - "tokio-tungstenite", - "tokio-util", - "tonic", - "tracing", - "tracing-appender", - "tracing-opentelemetry", - "tracing-subscriber", - "urlencoding", - "webpki-roots 1.0.9", -] - -[[package]] -name = "enum-ordinalize" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89dd01549b09589510cf0647475075d12071456586d70f5c75c98ae2a5537677" -dependencies = [ - "enum-ordinalize-derive", -] - -[[package]] -name = "enum-ordinalize-derive" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65863d15a4ce2888bd2f0f543cc963d3879c3a022c8ee43f6141d479a3ac815" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "eyre" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" -dependencies = [ - "indenter", - "once_cell", -] - -[[package]] -name = "fastrand" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" - -[[package]] -name = "fastrlp" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" -dependencies = [ - "arrayvec", - "auto_impl", - "bytes", -] - -[[package]] -name = "fastrlp" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" -dependencies = [ - "arrayvec", - "auto_impl", - "bytes", -] - -[[package]] -name = "ff" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" - -[[package]] -name = "fixed-cache" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fe63500644ef0269fe6b744e7e5dc5c20b5eebf3d881bc2be53f194636f6583" -dependencies = [ - "equivalent", - "rapidhash", -] - -[[package]] -name = "fixed-hash" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" -dependencies = [ - "byteorder", - "rand 0.8.7", - "rustc-hex", - "static_assertions", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - -[[package]] -name = "futures" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" - -[[package]] -name = "futures-executor" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" - -[[package]] -name = "futures-macro" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "futures-sink" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" - -[[package]] -name = "futures-task" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" - -[[package]] -name = "futures-util" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" -dependencies = [ - "typenum", - "version_check", - "zeroize", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi 5.3.0", - "wasip2", -] - -[[package]] -name = "getrandom" -version = "0.4.3" -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 = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "h2" -version = "0.4.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap 2.14.0", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hex-conservative" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" -dependencies = [ - "arrayvec", -] - -[[package]] -name = "hex-conservative" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35431185f361ccf3ffc58254628af5f1f5d5f28531da2e02e5d6c82bbc282a10" -dependencies = [ - "arrayvec", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "http" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hybrid-array" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" -dependencies = [ - "typenum", -] - -[[package]] -name = "hyper" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots 1.0.9", -] - -[[package]] -name = "hyper-timeout" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" -dependencies = [ - "hyper", - "hyper-util", - "pin-project-lite", - "tokio", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "system-configuration", - "tokio", - "tower-service", - "tracing", - "windows-registry", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" - -[[package]] -name = "icu_properties" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" - -[[package]] -name = "icu_provider" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "impl-codec" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" -dependencies = [ - "parity-scale-codec", -] - -[[package]] -name = "impl-trait-for-tuples" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "indenter" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", -] - -[[package]] -name = "ipnet" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "js-sys" -version = "0.3.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" -dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", -] - -[[package]] -name = "k256" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" -dependencies = [ - "cfg-if", - "ecdsa", - "elliptic-curve", - "once_cell", - "sha2", -] - -[[package]] -name = "keccak" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffd9697dc4a9a62e2da93389f34400b77a28f0287711263cabb203b3ccb9c0e4" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", -] - -[[package]] -name = "keccak-asm" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5dc2c0d691cbf7595cde551ced329cca99c2387c2cbc97754c5d0cd045d3ee" -dependencies = [ - "digest 0.10.7", - "sha3-asm", -] - -[[package]] -name = "konst" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" -dependencies = [ - "konst_macro_rules", -] - -[[package]] -name = "konst_macro_rules" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.189" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" - -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - -[[package]] -name = "matchit" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "num-bigint" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" -dependencies = [ - "num-integer", - "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.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "opentelemetry" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" -dependencies = [ - "futures-core", - "futures-sink", - "js-sys", - "pin-project-lite", - "thiserror", - "tracing", -] - -[[package]] -name = "opentelemetry-appender-tracing" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef6a1ac5ca3accf562b8c306fa8483c85f4390f768185ab775f242f7fe8fdcc2" -dependencies = [ - "opentelemetry", - "tracing", - "tracing-core", - "tracing-subscriber", -] - -[[package]] -name = "opentelemetry-http" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" -dependencies = [ - "async-trait", - "bytes", - "http", - "opentelemetry", - "reqwest", -] - -[[package]] -name = "opentelemetry-otlp" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f69cd6acbb9af919df949cd1ec9e5e7fdc2ef15d234b6b795aaa525cc02f71f" -dependencies = [ - "http", - "opentelemetry", - "opentelemetry-http", - "opentelemetry-proto", - "opentelemetry_sdk", - "prost", - "reqwest", - "thiserror", - "tracing", -] - -[[package]] -name = "opentelemetry-proto" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" -dependencies = [ - "opentelemetry", - "opentelemetry_sdk", - "prost", - "tonic", - "tonic-prost", -] - -[[package]] -name = "opentelemetry-semantic-conventions" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e62e29dfe041afb8ed2a6c9737ab57db4907285d999ef8ad3a59092a36bdc846" - -[[package]] -name = "opentelemetry_sdk" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" -dependencies = [ - "futures-channel", - "futures-executor", - "futures-util", - "opentelemetry", - "percent-encoding", - "rand 0.9.5", - "thiserror", - "tokio", - "tokio-stream", -] - -[[package]] -name = "parity-scale-codec" -version = "3.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" -dependencies = [ - "arrayvec", - "bitvec", - "byte-slice-cast", - "const_format", - "impl-trait-for-tuples", - "parity-scale-codec-derive", - "rustversion", - "serde", -] - -[[package]] -name = "parity-scale-codec-derive" -version = "3.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pest" -version = "2.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df728be843c7070fab6ab7c328c4e9e9d78e23bf749c0669c86ee7ebfa050a2" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "pointers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dcdc93847ad24990939cce6e1804361e903efcb5f99daa5abd87943a9d6d7ba" - -[[package]] -name = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "primitive-types" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" -dependencies = [ - "fixed-hash", - "impl-codec", - "uint", -] - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "proptest" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" -dependencies = [ - "bitflags", - "num-traits", - "rand 0.9.5", - "rand_chacha 0.9.0", - "rand_xorshift", - "regex-syntax", - "unarray", -] - -[[package]] -name = "prost" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-derive" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" -dependencies = [ - "anyhow", - "itertools 0.14.0", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "quinn" -version = "0.11.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" -dependencies = [ - "bytes", - "getrandom 0.4.3", - "lru-slab", - "rand 0.10.2", - "rand_pcg", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.61.2", -] - -[[package]] -name = "quote" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - -[[package]] -name = "rand" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" -dependencies = [ - "chacha20", - "getrandom 0.4.3", - "rand_core 0.10.1", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "rand_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 = "rand_xorshift" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" -dependencies = [ - "rand_core 0.9.5", -] - -[[package]] -name = "rapidhash" -version = "4.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5da7e78a036ce858e8d55b7e7dc8ba3a88b78350fd2155d3591bbd966b58589e" -dependencies = [ - "rustversion", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - -[[package]] -name = "ref-cast" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "regex-automata" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - -[[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64", - "bytes", - "encoding_rs", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "mime", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots 1.0.9", -] - -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac", - "subtle", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rlp" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" -dependencies = [ - "bytes", - "rustc-hex", -] - -[[package]] -name = "ruint" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e99bff0393163bb25029a6af25d3d8d202ba5b5438a74d1bd8789f5c822970" -dependencies = [ - "alloy-rlp", - "ark-ff 0.3.0", - "ark-ff 0.4.2", - "ark-ff 0.5.0", - "ark-ff 0.6.0", - "bytes", - "fastrlp 0.3.1", - "fastrlp 0.4.0", - "num-bigint", - "num-integer", - "num-traits", - "parity-scale-codec", - "primitive-types", - "proptest", - "rand 0.8.7", - "rand 0.9.5", - "rlp", - "ruint-macro", - "serde_core", - "valuable", - "zeroize", -] - -[[package]] -name = "ruint-macro" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" - -[[package]] -name = "rustc-hash" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" - -[[package]] -name = "rustc-hex" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" - -[[package]] -name = "rustc_version" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" -dependencies = [ - "semver 0.11.0", -] - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver 1.0.28", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls" -version = "0.23.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" -dependencies = [ - "log", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -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 = "schemars" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct", - "der", - "generic-array", - "pkcs8", - "subtle", - "zeroize", -] - -[[package]] -name = "secp256k1" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c3c81b43dc2d8877c216a3fccf76677ee1ebccd429566d3e67447290d0c42b2" -dependencies = [ - "bitcoin_hashes", - "rand 0.9.5", - "secp256k1-sys", -] - -[[package]] -name = "secp256k1-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb913707158fadaf0d8702c2db0e857de66eb003ccfdda5924b5f5ac98efb38" -dependencies = [ - "cc", -] - -[[package]] -name = "semver" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" -dependencies = [ - "semver-parser", -] - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[package]] -name = "semver-parser" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2" -dependencies = [ - "pest", -] - -[[package]] -name = "serde" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "serde_json" -version = "1.0.151" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_path_to_error" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" -dependencies = [ - "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 = "serde_with" -version = "3.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" -dependencies = [ - "base64", - "bs58", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.14.0", - "schemars 0.9.0", - "schemars 1.2.2", - "serde_core", - "serde_json", - "time", -] - -[[package]] -name = "sha1" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - -[[package]] -name = "sha3" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" -dependencies = [ - "digest 0.11.3", - "keccak", -] - -[[package]] -name = "sha3-asm" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6287fd675f713484342a89cbf0a386abef5f15919cfad607e5e1f19e1e15331" -dependencies = [ - "cc", - "cfg-if", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest 0.10.7", - "rand_core 0.6.4", -] - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" - -[[package]] -name = "socket2" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "symlink" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "system-configuration" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" -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 = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[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 = "thiserror" -version = "2.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "thread_local" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "time" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" -dependencies = [ - "deranged", - "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.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" -dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-stream" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" -dependencies = [ - "futures-util", - "log", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tungstenite", - "webpki-roots 0.26.11", -] - -[[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", - "libc", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.25.13+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" -dependencies = [ - "indexmap 2.14.0", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.1.3+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" -dependencies = [ - "winnow", -] - -[[package]] -name = "tonic" -version = "0.14.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" -dependencies = [ - "async-trait", - "axum", - "base64", - "bytes", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-timeout", - "hyper-util", - "percent-encoding", - "pin-project", - "socket2", - "sync_wrapper", - "tokio", - "tokio-stream", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tonic-prost" -version = "0.14.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" -dependencies = [ - "bytes", - "prost", - "tonic", -] - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "indexmap 2.14.0", - "pin-project-lite", - "slab", - "sync_wrapper", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-http" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" -dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", - "url", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-appender" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" -dependencies = [ - "crossbeam-channel", - "symlink", - "thiserror", - "time", - "tracing-subscriber", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-opentelemetry" -version = "0.32.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc" -dependencies = [ - "js-sys", - "opentelemetry", - "smallvec", - "tracing", - "tracing-core", - "tracing-log", - "tracing-subscriber", - "web-time", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "tungstenite" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" -dependencies = [ - "bytes", - "data-encoding", - "http", - "httparse", - "log", - "rand 0.9.5", - "rustls", - "rustls-pki-types", - "sha1", - "thiserror", -] - -[[package]] -name = "typenum" -version = "1.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" - -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - -[[package]] -name = "uint" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" -dependencies = [ - "byteorder", - "crunchy", - "hex", - "static_assertions", -] - -[[package]] -name = "unarray" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[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 = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" -dependencies = [ - "getrandom 0.4.3", - "js-sys", - "serde_core", - "wasm-bindgen", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.127" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.127" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.127" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.119", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.127" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "web-sys" -version = "0.3.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.9", -] - -[[package]] -name = "webpki-roots" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" -dependencies = [ - "rustls-pki-types", -] - -[[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-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 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-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "winnow" -version = "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 = "writeable" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" - -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - -[[package]] -name = "yoke" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "zerofrom" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", - "synstructure", -] - -[[package]] -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" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "zmij" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index 829bb8c..25156ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "3" members = ["crates/client", "crates/protocol", "crates/proxy"] [workspace.package] -version = "0.1.10" +version = "0.1.11" edition = "2024" license = "Apache-2.0" publish = true @@ -11,17 +11,21 @@ homepage = "https://github.com/pathscale/agencyproxy" repository = "https://github.com/pathscale/agencyproxy" [workspace.dependencies] -agency-proxy-protocol = { version = "^0.1.9", path = "crates/protocol" } -agency-proxy-client = { version = "^0.1.9", path = "crates/client" } +agency-proxy-protocol = { version = "^0.1", path = "crates/protocol" } +agency-proxy-client = { version = "^0.1", path = "crates/client" } async-trait = "0.1" clap = { version = "4", features = ["derive"] } -endpoint-libs = { version = "^3", default-features = false, features = ["framed-transport"] } +# `nagoya-transport` is `NagoyaStream`, which presents a nagoya socket to the +# runtime-neutral framing directly, so no compat layer sits in between. +endpoint-libs = { version = "^3", default-features = false, features = ["framed-transport", "nagoya-transport"] } eyre = "0.6" futures = "0.3" +# `reactor` is sockets and signals; the scheduler, timers and `sync` are in the +# default set. +nagoya = { version = "^0.1", features = ["reactor"] } serde = { version = "1", features = ["derive"] } serde_json = "1" thiserror = "2" -tokio = { version = "1", features = ["fs", "io-util", "macros", "net", "rt-multi-thread", "sync", "time"] } [profile.release] strip = true diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index b2d6b57..a3e53a2 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -12,6 +12,6 @@ repository.workspace = true agency-proxy-protocol.workspace = true endpoint-libs.workspace = true futures.workspace = true +nagoya.workspace = true serde_json.workspace = true thiserror.workspace = true -tokio.workspace = true diff --git a/crates/client/src/broadcast.rs b/crates/client/src/broadcast.rs new file mode 100644 index 0000000..924d807 --- /dev/null +++ b/crates/client/src/broadcast.rs @@ -0,0 +1,262 @@ +//! A bounded broadcast channel whose slow receivers lag rather than block. +//! +//! This stands in for `tokio::sync::broadcast`, which [`Client::subscribe`] +//! returned. nagoya's `sync` has no broadcast, and the shape the callers depend +//! on is small enough to keep here rather than take a crate for: every receiver +//! sees every value sent after it subscribed, a sender never waits, and a +//! receiver that falls more than `capacity` values behind is told how many it +//! missed (`RecvError::Lagged`) and resumes at the oldest value still held. +//! +//! What differs from tokio: the capacity is used as given rather than rounded +//! up to a power of two, and waiting is a `nagoya::sync::Notify` wake of every +//! receiver per send rather than a per-receiver waiter list. Nothing here sends +//! fast enough for the second to matter, and neither changes what a receiver +//! observes. +//! +//! The proxy crate carries the same file. The protocol crate is the only one +//! both depend on, and it is deliberately vocabulary with no runtime behaviour. +//! +//! [`Client::subscribe`]: crate::Client::subscribe + +use nagoya::sync::Notify; +use std::{ + collections::VecDeque, + fmt, + sync::{Arc, Mutex, MutexGuard, PoisonError}, +}; + +/// A channel holding at most `capacity` values for its slowest receiver. +/// +/// A capacity of zero is treated as one, where tokio would panic. +pub fn channel(capacity: usize) -> (Sender, Receiver) { + let shared = Arc::new(Shared { + state: Mutex::new(State { + buffer: VecDeque::new(), + head: 0, + senders: 1, + receivers: 1, + }), + capacity: capacity.max(1), + changed: Notify::new(), + }); + ( + Sender { + shared: Arc::clone(&shared), + }, + Receiver { shared, next: 0 }, + ) +} + +struct Shared { + state: Mutex>, + capacity: usize, + /// Woken on every send and when the last sender goes. + changed: Notify, +} + +struct State { + buffer: VecDeque, + /// The sequence position of `buffer[0]`. + head: u64, + senders: usize, + receivers: usize, +} + +impl Shared { + fn lock(&self) -> MutexGuard<'_, State> { + // Nothing panics while holding this lock, and a poisoned buffer of + // cloned values is still a valid buffer. + self.state.lock().unwrap_or_else(PoisonError::into_inner) + } +} + +/// The sending half. Cloning it adds a sender; the channel closes when the +/// last one is dropped. +pub struct Sender { + shared: Arc>, +} + +/// A send found no receiver, and the value is handed back. +#[derive(Debug)] +pub struct SendError(pub T); + +impl fmt::Display for SendError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("the broadcast channel has no receivers") + } +} + +impl std::error::Error for SendError {} + +impl Sender { + /// Send `value` to every current receiver and return how many there are. + /// + /// Never waits. With no receiver the value is not kept, as with tokio, so a + /// later subscriber does not see it. + pub fn send(&self, value: T) -> Result> { + let receivers = { + let mut state = self.shared.lock(); + if state.receivers == 0 { + return Err(SendError(value)); + } + state.buffer.push_back(value); + if state.buffer.len() > self.shared.capacity { + state.buffer.pop_front(); + state.head += 1; + } + state.receivers + }; + self.shared.changed.notify_waiters(); + Ok(receivers) + } + + /// A receiver that sees every value sent from now on. + pub fn subscribe(&self) -> Receiver { + let mut state = self.shared.lock(); + state.receivers += 1; + let next = state.head + state.buffer.len() as u64; + Receiver { + shared: Arc::clone(&self.shared), + next, + } + } +} + +impl Clone for Sender { + fn clone(&self) -> Self { + self.shared.lock().senders += 1; + Self { + shared: Arc::clone(&self.shared), + } + } +} + +impl Drop for Sender { + fn drop(&mut self) { + let last = { + let mut state = self.shared.lock(); + state.senders -= 1; + state.senders == 0 + }; + if last { + // Parked receivers have drained what is buffered or will, and then + // they must learn the channel is closed rather than wait forever. + self.shared.changed.notify_waiters(); + } + } +} + +impl fmt::Debug for Sender { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.debug_struct("Sender").finish_non_exhaustive() + } +} + +/// Why [`Receiver::recv`] returned no value. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RecvError { + /// Every sender is gone and every buffered value has been received. + Closed, + /// This receiver fell behind and this many values were dropped before it + /// saw them. The next `recv` returns the oldest value still held. + Lagged(u64), +} + +impl fmt::Display for RecvError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Closed => formatter.write_str("the broadcast channel is closed"), + Self::Lagged(missed) => write!(formatter, "the receiver lagged by {missed} value(s)"), + } + } +} + +impl std::error::Error for RecvError {} + +/// The receiving half. Each receiver has its own position in the stream. +pub struct Receiver { + shared: Arc>, + /// The sequence position of the next value this receiver will return. + next: u64, +} + +impl Receiver { + /// The next value, waiting for one if none is buffered. + /// + /// Cancel safe: dropping the future before it completes loses nothing. + pub async fn recv(&mut self) -> Result { + let shared = Arc::clone(&self.shared); + loop { + // Created before the buffer is checked: `Notify` snapshots its + // broadcast generation here, so a send landing between the check + // and the await still wakes this receiver. + let changed = shared.changed.notified(); + if let Some(result) = take(&shared, &mut self.next) { + return result; + } + changed.await; + } + } +} + +fn take(shared: &Shared, next: &mut u64) -> Option> { + let state = shared.lock(); + if *next < state.head { + let missed = state.head - *next; + *next = state.head; + return Some(Err(RecvError::Lagged(missed))); + } + let offset = usize::try_from(*next - state.head).unwrap_or(usize::MAX); + if let Some(value) = state.buffer.get(offset) { + *next += 1; + return Some(Ok(value.clone())); + } + if state.senders == 0 { + return Some(Err(RecvError::Closed)); + } + None +} + +impl Drop for Receiver { + fn drop(&mut self) { + self.shared.lock().receivers -= 1; + } +} + +impl fmt::Debug for Receiver { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("Receiver") + .field("next", &self.next) + .finish_non_exhaustive() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_slow_receiver_is_told_how_far_it_lagged() { + nagoya::block_on(async { + let (sender, mut receiver) = channel(2); + for value in 1..=5 { + sender.send(value).expect("a receiver exists"); + } + assert_eq!(receiver.recv().await, Err(RecvError::Lagged(3))); + assert_eq!(receiver.recv().await, Ok(4)); + assert_eq!(receiver.recv().await, Ok(5)); + }); + } + + #[test] + fn dropping_the_last_sender_closes_after_the_buffer_drains() { + nagoya::block_on(async { + let (sender, mut receiver) = channel(2); + sender.send(1).expect("a receiver exists"); + drop(sender); + assert_eq!(receiver.recv().await, Ok(1)); + assert_eq!(receiver.recv().await, Err(RecvError::Closed)); + }); + } +} diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index 41d17b0..44bbfe5 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -1,15 +1,24 @@ +pub mod broadcast; + use agency_proxy_protocol::{ ClientFrame, ClientMessage, MAX_FRAME_BYTES, PROTOCOL_VERSION, ProtocolVersion, ServerFrame, ServerResponse, }; -use endpoint_libs::libs::ws::{WireMessage, transport::framed::framed_json_with_max_frame}; -use futures::{SinkExt, StreamExt}; -use std::{collections::BTreeMap, path::Path}; -use thiserror::Error; -use tokio::{ - net::UnixStream, - sync::{broadcast, mpsc, oneshot}, +// The neutral framing takes a `futures_io` stream, and `NagoyaStream` presents +// a nagoya socket as one directly, so there is no compat layer in between. +use endpoint_libs::libs::ws::{ + WireMessage, + transport::{framed::framed_json_neutral_with_max_frame, nagoya::NagoyaStream}, +}; +use futures::{ + SinkExt, StreamExt, + channel::{mpsc, oneshot}, + future::{Either, select}, }; +use nagoya::net::TcpStream; +use nagoya::reactor::{Addr, Handle}; +use std::{collections::BTreeMap, os::unix::ffi::OsStrExt, path::Path}; +use thiserror::Error; #[derive(Debug, Error)] pub enum ClientError { @@ -30,18 +39,39 @@ struct PendingRequest { #[derive(Clone, Debug)] pub struct Client { - requests: mpsc::Sender, + // Unbounded where tokio's was bounded at 64. `futures`' bounded sender + // needs `&mut self` to send, and cloning one per request would give every + // clone its own guaranteed slot, which is no bound either. The drive task + // is the only consumer and every request waits for its answer, so the + // queue is bounded by the number of callers awaiting `request`. + requests: mpsc::UnboundedSender, events: broadcast::Sender, version: ProtocolVersion, } impl Client { - pub async fn connect(socket_path: impl AsRef) -> Result { - let stream = UnixStream::connect(socket_path).await?; - let transport = framed_json_with_max_frame(stream, MAX_FRAME_BYTES + 1); - let (requests, request_rx) = mpsc::channel(64); + /// Connect to the proxy's Unix socket and negotiate the protocol. + /// + /// The connection is registered on `reactor`, and makes progress only + /// while that reactor is polled: the caller owns it and must keep it + /// running for as long as the client is in use. A threaded reactor + /// (`Reactor::start`) is the usual choice, since nothing else here polls. + pub async fn connect( + socket_path: impl AsRef, + reactor: &Handle, + ) -> Result { + let address = Addr::path(socket_path.as_ref().as_os_str().as_bytes()) + .map_err(std::io::Error::from)?; + let stream = TcpStream::connect(address, reactor) + .await + .map_err(std::io::Error::from)?; + let transport = + framed_json_neutral_with_max_frame(NagoyaStream::new(stream), MAX_FRAME_BYTES + 1); + let (requests, request_rx) = mpsc::unbounded(); let (events, _) = broadcast::channel(512); - tokio::spawn(drive(transport, request_rx, events.clone())); + // Detached, as tokio's spawn was: the task ends when every `Client` + // clone is gone and the request channel closes, or when the proxy does. + drop(nagoya::spawn(drive(transport, request_rx, events.clone()))); let mut client = Self { requests, events, @@ -77,8 +107,7 @@ impl Client { pub async fn request(&self, message: ClientMessage) -> Result { let (response, received) = oneshot::channel(); self.requests - .send(PendingRequest { message, response }) - .await + .unbounded_send(PendingRequest { message, response }) .map_err(|_| ClientError::Closed)?; received.await.map_err(|_| ClientError::Closed)? } @@ -86,7 +115,7 @@ impl Client { async fn drive( mut transport: T, - mut requests: mpsc::Receiver, + mut requests: mpsc::UnboundedReceiver, events: broadcast::Sender, ) where T: futures::Sink @@ -97,40 +126,66 @@ async fn drive( let mut next_request_id = 1u64; let mut pending = BTreeMap::>>::new(); let stopped = loop { - tokio::select! { - request = requests.recv() => { - let Some(request) = request else { break "request channel closed".to_string() }; + // Both halves are cancel safe: the framed stream keeps a partial frame + // in its own buffer, and an unreceived request stays in the channel. + // The losing future is dropped at the end of this statement, which + // releases `transport` for the send below. + let step = match select(requests.next(), transport.next()).await { + Either::Left((request, _)) => Either::Left(request), + Either::Right((incoming, _)) => Either::Right(incoming), + }; + match step { + Either::Left(request) => { + let Some(request) = request else { + break "request channel closed".to_string(); + }; let request_id = next_request_id; next_request_id = next_request_id.wrapping_add(1).max(1); - let frame = ClientFrame { request_id, message: request.message }; + let frame = ClientFrame { + request_id, + message: request.message, + }; let encoded = match serde_json::to_string(&frame) { Ok(encoded) => encoded, Err(error) => { - let _ = request.response.send(Err(ClientError::Protocol(error.to_string()))); + let _ = request + .response + .send(Err(ClientError::Protocol(error.to_string()))); continue; } }; if let Err(error) = transport.send(WireMessage::Text(encoded.into())).await { let detail = error.to_string(); - let _ = request.response.send(Err(ClientError::Transport(detail.clone()))); + let _ = request + .response + .send(Err(ClientError::Transport(detail.clone()))); break detail; } pending.insert(request_id, request.response); } - incoming = transport.next() => { - let Some(incoming) = incoming else { break "proxy closed the connection".into() }; + Either::Right(incoming) => { + let Some(incoming) = incoming else { + break "proxy closed the connection".into(); + }; let message = match incoming { Ok(message) => message, Err(error) => break error.to_string(), }; - if message.is_close() { break "proxy closed the connection".into(); } - let Some(text) = message.as_text() else { continue }; + if message.is_close() { + break "proxy closed the connection".into(); + } + let Some(text) = message.as_text() else { + continue; + }; let frame: ServerFrame = match serde_json::from_str(text) { Ok(frame) => frame, Err(error) => break error.to_string(), }; match frame { - ServerFrame::Response { request_id, response } => { + ServerFrame::Response { + request_id, + response, + } => { if let Some(waiter) = pending.remove(&request_id) { let _ = waiter.send(Ok(response)); } diff --git a/crates/proxy/Cargo.toml b/crates/proxy/Cargo.toml index bccb9b6..46ce0d9 100644 --- a/crates/proxy/Cargo.toml +++ b/crates/proxy/Cargo.toml @@ -9,23 +9,25 @@ homepage.workspace = true repository.workspace = true [dependencies] -agent-abstraction = "0.4.21" +agent-abstraction = "^0.5" agency-proxy-protocol.workspace = true async-trait.workspace = true clap.workspace = true endpoint-libs = { workspace = true, features = ["ws-http1"] } eyre.workspace = true futures.workspace = true +nagoya.workspace = true serde.workspace = true serde_json.workspace = true thiserror.workspace = true -tokio.workspace = true [dev-dependencies] agency-proxy-client.workspace = true +# The WebSocket client the MCP tests drive the server with: nago-wss over a +# nagoya socket, the same stack the server speaks, and no tokio. +endpoint-libs = { workspace = true, features = ["ws-client"] } libc = "0.2" tempfile = "3" -tokio-tungstenite = "0.29" [[bin]] name = "agency-proxy" diff --git a/crates/proxy/src/broadcast.rs b/crates/proxy/src/broadcast.rs new file mode 100644 index 0000000..43cfc25 --- /dev/null +++ b/crates/proxy/src/broadcast.rs @@ -0,0 +1,286 @@ +//! A bounded broadcast channel whose slow receivers lag rather than block. +//! +//! This stands in for `tokio::sync::broadcast`, which the run journal and its +//! attachments were built on. nagoya's `sync` has no broadcast, and the shape +//! the callers depend on is small enough to keep here rather than take a crate +//! for: every receiver sees every value sent after it subscribed, a sender never +//! waits, and a receiver that falls more than `capacity` values behind is told +//! how many it missed (`RecvError::Lagged`) and resumes at the oldest value +//! still held. The callers answer a lag by replaying from the run journal, so +//! the lag report is the part that has to be exact. +//! +//! What differs from tokio: the capacity is used as given rather than rounded +//! up to a power of two, and waiting is a `nagoya::sync::Notify` wake of every +//! receiver per send rather than a per-receiver waiter list. Nothing here sends +//! fast enough for the second to matter, and neither changes what a receiver +//! observes. +//! +//! The client crate carries the same file. The protocol crate is the only one +//! both depend on, and it is deliberately vocabulary with no runtime behaviour. + +use nagoya::sync::Notify; +use std::{ + collections::VecDeque, + fmt, + sync::{Arc, Mutex, MutexGuard, PoisonError}, +}; + +/// A channel holding at most `capacity` values for its slowest receiver. +/// +/// A capacity of zero is treated as one, where tokio would panic. +pub fn channel(capacity: usize) -> (Sender, Receiver) { + let shared = Arc::new(Shared { + state: Mutex::new(State { + buffer: VecDeque::new(), + head: 0, + senders: 1, + receivers: 1, + }), + capacity: capacity.max(1), + changed: Notify::new(), + }); + ( + Sender { + shared: Arc::clone(&shared), + }, + Receiver { shared, next: 0 }, + ) +} + +struct Shared { + state: Mutex>, + capacity: usize, + /// Woken on every send and when the last sender goes. + changed: Notify, +} + +struct State { + buffer: VecDeque, + /// The sequence position of `buffer[0]`. + head: u64, + senders: usize, + receivers: usize, +} + +impl Shared { + fn lock(&self) -> MutexGuard<'_, State> { + // Nothing panics while holding this lock, and a poisoned buffer of + // cloned values is still a valid buffer. + self.state.lock().unwrap_or_else(PoisonError::into_inner) + } +} + +/// The sending half. Cloning it adds a sender; the channel closes when the +/// last one is dropped. +pub struct Sender { + shared: Arc>, +} + +/// A send found no receiver, and the value is handed back. +#[derive(Debug)] +pub struct SendError(pub T); + +impl fmt::Display for SendError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("the broadcast channel has no receivers") + } +} + +impl std::error::Error for SendError {} + +impl Sender { + /// Send `value` to every current receiver and return how many there are. + /// + /// Never waits. With no receiver the value is not kept, as with tokio, so a + /// later subscriber does not see it. + pub fn send(&self, value: T) -> Result> { + let receivers = { + let mut state = self.shared.lock(); + if state.receivers == 0 { + return Err(SendError(value)); + } + state.buffer.push_back(value); + if state.buffer.len() > self.shared.capacity { + state.buffer.pop_front(); + state.head += 1; + } + state.receivers + }; + self.shared.changed.notify_waiters(); + Ok(receivers) + } + + /// A receiver that sees every value sent from now on. + pub fn subscribe(&self) -> Receiver { + let mut state = self.shared.lock(); + state.receivers += 1; + let next = state.head + state.buffer.len() as u64; + Receiver { + shared: Arc::clone(&self.shared), + next, + } + } +} + +impl Clone for Sender { + fn clone(&self) -> Self { + self.shared.lock().senders += 1; + Self { + shared: Arc::clone(&self.shared), + } + } +} + +impl Drop for Sender { + fn drop(&mut self) { + let last = { + let mut state = self.shared.lock(); + state.senders -= 1; + state.senders == 0 + }; + if last { + // Parked receivers have drained what is buffered or will, and then + // they must learn the channel is closed rather than wait forever. + self.shared.changed.notify_waiters(); + } + } +} + +impl fmt::Debug for Sender { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.debug_struct("Sender").finish_non_exhaustive() + } +} + +/// Why [`Receiver::recv`] returned no value. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RecvError { + /// Every sender is gone and every buffered value has been received. + Closed, + /// This receiver fell behind and this many values were dropped before it + /// saw them. The next `recv` returns the oldest value still held. + Lagged(u64), +} + +impl fmt::Display for RecvError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Closed => formatter.write_str("the broadcast channel is closed"), + Self::Lagged(missed) => write!(formatter, "the receiver lagged by {missed} value(s)"), + } + } +} + +impl std::error::Error for RecvError {} + +/// The receiving half. Each receiver has its own position in the stream. +pub struct Receiver { + shared: Arc>, + /// The sequence position of the next value this receiver will return. + next: u64, +} + +impl Receiver { + /// The next value, waiting for one if none is buffered. + /// + /// Cancel safe: dropping the future before it completes loses nothing. + pub async fn recv(&mut self) -> Result { + let shared = Arc::clone(&self.shared); + loop { + // Created before the buffer is checked: `Notify` snapshots its + // broadcast generation here, so a send landing between the check + // and the await still wakes this receiver. + let changed = shared.changed.notified(); + if let Some(result) = take(&shared, &mut self.next) { + return result; + } + changed.await; + } + } +} + +fn take(shared: &Shared, next: &mut u64) -> Option> { + let state = shared.lock(); + if *next < state.head { + let missed = state.head - *next; + *next = state.head; + return Some(Err(RecvError::Lagged(missed))); + } + let offset = usize::try_from(*next - state.head).unwrap_or(usize::MAX); + if let Some(value) = state.buffer.get(offset) { + *next += 1; + return Some(Ok(value.clone())); + } + if state.senders == 0 { + return Some(Err(RecvError::Closed)); + } + None +} + +impl Drop for Receiver { + fn drop(&mut self) { + self.shared.lock().receivers -= 1; + } +} + +impl fmt::Debug for Receiver { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("Receiver") + .field("next", &self.next) + .finish_non_exhaustive() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_receiver_sees_values_sent_after_it_subscribed() { + nagoya::block_on(async { + let (sender, mut first) = channel(4); + sender.send(1).expect("a receiver exists"); + let mut second = sender.subscribe(); + sender.send(2).expect("receivers exist"); + assert_eq!(first.recv().await, Ok(1)); + assert_eq!(first.recv().await, Ok(2)); + assert_eq!(second.recv().await, Ok(2)); + }); + } + + #[test] + fn a_slow_receiver_is_told_how_far_it_lagged() { + nagoya::block_on(async { + let (sender, mut receiver) = channel(2); + for value in 1..=5 { + sender.send(value).expect("a receiver exists"); + } + assert_eq!(receiver.recv().await, Err(RecvError::Lagged(3))); + assert_eq!(receiver.recv().await, Ok(4)); + assert_eq!(receiver.recv().await, Ok(5)); + }); + } + + #[test] + fn dropping_the_last_sender_closes_after_the_buffer_drains() { + nagoya::block_on(async { + let (sender, mut receiver) = channel(2); + sender.send(1).expect("a receiver exists"); + drop(sender); + assert_eq!(receiver.recv().await, Ok(1)); + assert_eq!(receiver.recv().await, Err(RecvError::Closed)); + }); + } + + #[test] + fn a_parked_receiver_wakes_on_send() { + nagoya::block_on(async { + let (sender, mut receiver) = channel(2); + let waiter = nagoya::spawn(async move { receiver.recv().await }); + nagoya::sleep(std::time::Duration::from_millis(20)).await; + sender.send(7).expect("a receiver exists"); + assert_eq!(waiter.await, Some(Ok(7))); + }); + } +} diff --git a/crates/proxy/src/config.rs b/crates/proxy/src/config.rs index 24110ef..ce54d71 100644 --- a/crates/proxy/src/config.rs +++ b/crates/proxy/src/config.rs @@ -25,18 +25,9 @@ pub enum ConnectionConfig { authentication_key: String, #[serde(default)] allowed_origins: Vec, - #[serde(default)] - tls: Option, }, } -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct TlsConfig { - pub certificates: Vec, - pub private_key: PathBuf, -} - #[derive(Debug, Error)] pub enum ConfigError { #[error("could not read AgencyProxy config: {0}")] @@ -54,8 +45,13 @@ pub enum ConfigError { } impl ProxyConfig { + /// Read and validate the config file. + /// + /// A blocking `std::fs::read`: this is one small file read once at + /// startup, before any socket exists, so there is nothing for it to stall. + /// It stays `async` so callers do not change. pub async fn read(path: &std::path::Path) -> Result { - let bytes = tokio::fs::read(path).await.map_err(ConfigError::Read)?; + let bytes = std::fs::read(path).map_err(ConfigError::Read)?; let config: Self = serde_json::from_slice(&bytes).map_err(ConfigError::Parse)?; config.validate()?; Ok(config) @@ -66,7 +62,6 @@ impl ProxyConfig { address, authentication_key, allowed_origins, - tls: _, } = &self.connection else { return Ok(()); @@ -119,7 +114,6 @@ mod tests { address: "0.0.0.0:17820".parse().expect("address should parse"), authentication_key: KEY.into(), allowed_origins: Vec::new(), - tls: None, }, }; assert!(matches!( @@ -132,7 +126,6 @@ mod tests { address: "127.0.0.1:17820".parse().expect("address should parse"), authentication_key: "short".into(), allowed_origins: Vec::new(), - tls: None, }, }; assert!(matches!( diff --git a/crates/proxy/src/flag.rs b/crates/proxy/src/flag.rs new file mode 100644 index 0000000..0400af0 --- /dev/null +++ b/crates/proxy/src/flag.rs @@ -0,0 +1,47 @@ +//! A set-once flag that can be awaited. +//! +//! This is what the proxy used `tokio::sync::watch::channel(false)` for: the +//! daemon's shutdown request and each run's completion. Both only ever went +//! from `false` to `true` and were only ever waited on for `true`, so the +//! channel's value, versioning and `borrow` were never the point. nagoya's +//! `sync` has no watch; an atomic plus a `Notify` is the whole of what was used. +//! +//! One difference from the watch it replaces: a watch receiver saw an error +//! when its sender was dropped and its waiters broke out on that. Here the +//! waiter holds the flag itself, so there is no sender to lose; every place +//! that dropped the sender either set it first or never let it go. + +use nagoya::sync::Notify; +use std::sync::atomic::{AtomicBool, Ordering}; + +#[derive(Debug, Default)] +pub(crate) struct Flag { + set: AtomicBool, + changed: Notify, +} + +impl Flag { + /// Set the flag and wake everything waiting on it. Idempotent. + pub(crate) fn set(&self) { + self.set.store(true, Ordering::SeqCst); + self.changed.notify_waiters(); + } + + pub(crate) fn is_set(&self) -> bool { + self.set.load(Ordering::SeqCst) + } + + /// Resolve once the flag is set, immediately if it already is. + pub(crate) async fn wait(&self) { + loop { + // Created before the check: `Notify` snapshots its broadcast + // generation here, so a `set` landing between the check and the + // await still wakes this waiter. + let changed = self.changed.notified(); + if self.is_set() { + return; + } + changed.await; + } + } +} diff --git a/crates/proxy/src/lib.rs b/crates/proxy/src/lib.rs index 9c14d97..d9ba3c8 100644 --- a/crates/proxy/src/lib.rs +++ b/crates/proxy/src/lib.rs @@ -1,6 +1,8 @@ //! Local AgencyProxy server and provider-runtime boundary. +pub mod broadcast; mod config; +mod flag; mod runtime; mod web; @@ -8,19 +10,32 @@ use agency_proxy_protocol::{ Capability, ClientFrame, ClientMessage, ErrorCode, MAX_FRAME_BYTES, PROTOCOL_VERSION, RunId, ServerFrame, ServerResponse, ShutdownMode, }; -use endpoint_libs::libs::ws::{WireMessage, transport::framed::framed_json_with_max_frame}; -use futures::{Sink, SinkExt, Stream, StreamExt}; +// The neutral framing takes a `futures_io` stream, and `NagoyaStream` presents +// a nagoya socket as one directly, so there is no compat layer in between. +use endpoint_libs::libs::ws::{ + WireMessage, + transport::{framed::framed_json_neutral_with_max_frame, nagoya::NagoyaStream}, +}; +use flag::Flag; +use futures::{ + Sink, SinkExt, Stream, StreamExt, + channel::mpsc, + future::{AbortHandle, Either, abortable, select}, +}; +use nagoya::net::{TcpListener, TcpStream}; +use nagoya::reactor::{Addr, Handle}; use std::{ collections::BTreeMap, + os::unix::ffi::OsStrExt, path::{Path, PathBuf}, + pin::pin, sync::Arc, }; use thiserror::Error; -use tokio::net::{UnixListener, UnixStream}; -pub use config::{ConfigError, ConnectionConfig, ProxyConfig, TlsConfig}; +pub use config::{ConfigError, ConnectionConfig, ProxyConfig}; pub use runtime::{Attachment, RuntimeError, RuntimeRegistry, SequencedEvent}; -pub use web::{WebSocketConfig, WebSocketTlsConfig, serve_websocket}; +pub use web::{WebSocketConfig, serve_websocket}; #[derive(Debug, Error)] pub enum Error { @@ -38,11 +53,15 @@ pub enum Error { #[derive(Debug)] pub struct ProxyServer { - listener: UnixListener, + listener: TcpListener, registry: RuntimeRegistry, socket_path: PathBuf, - shutdown: tokio::sync::watch::Sender, - lifecycle: Arc>, + // Was a `tokio::sync::watch::Sender`; see `flag`. + shutdown: Arc, + // An async lock because the Shutdown and StartRun admission check holds it + // across the acknowledgement write. nagoya's `sync` has no `Mutex`; its + // `RwLock` taken only for writing is one. + lifecycle: Arc>, } #[derive(Debug, Default)] @@ -51,24 +70,35 @@ struct Lifecycle { } impl ProxyServer { - pub async fn bind(socket_path: impl AsRef) -> Result { - Self::bind_with_registry(socket_path, RuntimeRegistry::default()).await + /// Bind with a fresh registry whose provider runs use the same reactor. + /// + /// The listener and every accepted connection are registered on + /// `reactor`, which the caller owns and must keep running for as long as + /// the server is in use. + pub async fn bind(socket_path: impl AsRef, reactor: &Handle) -> Result { + Self::bind_with_registry(socket_path, RuntimeRegistry::new(reactor.clone()), reactor).await } + /// Bind with an existing registry. `reactor` carries the listener and its + /// connections; the registry keeps the handle it was built with for + /// provider processes. pub async fn bind_with_registry( socket_path: impl AsRef, registry: RuntimeRegistry, + reactor: &Handle, ) -> Result { let socket_path = socket_path.as_ref().to_path_buf(); prepare_socket_path(&socket_path).await?; - let listener = UnixListener::bind(&socket_path)?; + // nagoya's socket type is `TcpListener` for every address family; a + // path address makes it a Unix-domain listener. + let listener = TcpListener::bind(unix_address(&socket_path)?, reactor) + .map_err(std::io::Error::from)?; set_permissions(&socket_path, 0o600)?; - let (shutdown, _) = tokio::sync::watch::channel(false); Ok(Self { listener, registry, socket_path, - shutdown, + shutdown: Arc::default(), lifecycle: Arc::default(), }) } @@ -79,45 +109,56 @@ impl ProxyServer { } pub async fn serve(self) -> Result<(), Error> { - let mut shutdown = self.shutdown.subscribe(); loop { - let accepted = tokio::select! { - accepted = self.listener.accept() => accepted, - changed = shutdown.changed() => { - if changed.is_ok() && *shutdown.borrow() { - return Ok(()); - } - continue; - } + // The shutdown request is polled first so a steady stream of + // connections cannot keep a stopping daemon alive; tokio's + // `select!` picked a random branch, which had the same property on + // average. Accept is cancel safe: losing leaves any pending + // connection in the kernel's backlog, closed with the listener. + let accepted = match select(pin!(self.shutdown.wait()), self.listener.accept()).await { + Either::Left(((), _)) => return Ok(()), + Either::Right((accepted, _)) => accepted, }; - let (stream, _) = accepted?; + let (stream, _) = accepted.map_err(std::io::Error::from)?; let registry = self.registry.clone(); - let request_shutdown = self.shutdown.clone(); + let request_shutdown = Arc::clone(&self.shutdown); let lifecycle = self.lifecycle.clone(); - tokio::spawn(async move { + // Detached, as tokio's spawn was: a connection ends on its own. + drop(nagoya::spawn(async move { if let Err(error) = handle_connection(stream, registry, request_shutdown, lifecycle).await { eprintln!("AgencyProxy connection failed: {error}"); } - }); + })); } } } +fn unix_address(socket_path: &Path) -> Result { + Addr::path(socket_path.as_os_str().as_bytes()) + .map_err(|error| Error::Io(std::io::Error::from(error))) +} + impl Drop for ProxyServer { fn drop(&mut self) { let _ = std::fs::remove_file(&self.socket_path); } } +/// Make the socket's directory private and clear a stale socket from a +/// previous daemon. +/// +/// Blocking `std` calls rather than tokio's thread-pool wrappers: these are +/// three metadata operations and one Unix-domain connect, run once at bind +/// time, and a Unix-domain connect completes or fails inside the call. async fn prepare_socket_path(socket_path: &Path) -> Result<(), Error> { let parent = socket_path .parent() .ok_or_else(|| Error::UnsafeSocketPath(socket_path.to_path_buf()))?; - tokio::fs::create_dir_all(parent).await?; + std::fs::create_dir_all(parent)?; set_permissions(parent, 0o700)?; - let metadata = match tokio::fs::symlink_metadata(socket_path).await { + let metadata = match std::fs::symlink_metadata(socket_path) { Ok(metadata) => metadata, Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()), Err(error) => return Err(error.into()), @@ -125,10 +166,10 @@ async fn prepare_socket_path(socket_path: &Path) -> Result<(), Error> { if !std::os::unix::fs::FileTypeExt::is_socket(&metadata.file_type()) { return Err(Error::UnsafeSocketPath(socket_path.to_path_buf())); } - if UnixStream::connect(socket_path).await.is_ok() { + if std::os::unix::net::UnixStream::connect(socket_path).is_ok() { return Err(Error::AlreadyRunning(socket_path.to_path_buf())); } - tokio::fs::remove_file(socket_path).await?; + std::fs::remove_file(socket_path)?; Ok(()) } @@ -139,12 +180,13 @@ fn set_permissions(path: &Path, mode: u32) -> Result<(), Error> { } async fn handle_connection( - stream: UnixStream, + stream: TcpStream, registry: RuntimeRegistry, - request_shutdown: tokio::sync::watch::Sender, - lifecycle: Arc>, + request_shutdown: Arc, + lifecycle: Arc>, ) -> Result<(), Error> { - let mut transport = framed_json_with_max_frame(stream, MAX_FRAME_BYTES + 1); + let mut transport = + framed_json_neutral_with_max_frame(NagoyaStream::new(stream), MAX_FRAME_BYTES + 1); let Some(first) = receive_client(&mut transport).await? else { return Ok(()); }; @@ -187,31 +229,63 @@ async fn handle_connection( ) .await?; - let (outbound, mut outgoing) = tokio::sync::mpsc::unbounded_channel::(); - let mut attachments = BTreeMap::>::new(); + let (outbound, mut outgoing) = mpsc::unbounded::(); + // Abort handles rather than join handles: nagoya's `JoinHandle::cancel` + // consumes the handle, and these are aborted from a map. `abortable` stops + // the task at its next suspension point, as tokio's `abort` did. + let mut attachments = BTreeMap::::new(); loop { - tokio::select! { - Some(frame) = outgoing.recv() => send_server(&mut transport, &frame).await?, - incoming = receive_client(&mut transport) => { + // The outgoing queue is never exhausted, because this function holds + // `outbound`, so its branch never disables itself the way a failed + // `Some(frame)` pattern did under `tokio::select!`. Both halves are + // cancel safe: the framed stream keeps a partial frame in its own + // buffer. The losing future is dropped at the end of this statement, + // which releases `transport` for the writes below. + let step = match select(outgoing.next(), pin!(receive_client(&mut transport))).await { + Either::Left((frame, _)) => Either::Left(frame), + Either::Right((incoming, _)) => Either::Right(incoming), + }; + match step { + Either::Left(frame) => { + if let Some(frame) = frame { + send_server(&mut transport, &frame).await?; + } + } + Either::Right(incoming) => { let Some(frame) = incoming? else { break }; match frame.message { ClientMessage::ListRuns => { - send_response(&mut transport, frame.request_id, ServerResponse::Runs { - runs: registry.list().await, - }).await?; + send_response( + &mut transport, + frame.request_id, + ServerResponse::Runs { + runs: registry.list().await, + }, + ) + .await?; } ClientMessage::ProbeProviders => { - send_response(&mut transport, frame.request_id, ServerResponse::Providers { - providers: registry.probe_providers().await, - }).await?; + send_response( + &mut transport, + frame.request_id, + ServerResponse::Providers { + providers: registry.probe_providers().await, + }, + ) + .await?; } ClientMessage::ReadAccountUsage => { - send_response(&mut transport, frame.request_id, ServerResponse::AccountUsage { - providers: registry.account_usage().await, - }).await?; + send_response( + &mut transport, + frame.request_id, + ServerResponse::AccountUsage { + providers: registry.account_usage().await, + }, + ) + .await?; } ClientMessage::ShutdownIfIdle => { - let mut lifecycle = lifecycle.lock().await; + let mut lifecycle = lifecycle.write().await; let active = registry.active_count().await; if active > 0 { send_error( @@ -219,145 +293,251 @@ async fn handle_connection( frame.request_id, ErrorCode::Conflict, &format!("AgencyProxy has {active} active run(s)"), - ).await?; + ) + .await?; continue; } lifecycle.stopping = true; - send_response( - &mut transport, - frame.request_id, - ServerResponse::Accepted, - ).await?; - let _ = request_shutdown.send(true); + send_response(&mut transport, frame.request_id, ServerResponse::Accepted) + .await?; + request_shutdown.set(); return Ok(()); } ClientMessage::Shutdown { mode } => { - let mut lifecycle = lifecycle.lock().await; + let mut lifecycle = lifecycle.write().await; if lifecycle.stopping { send_error( &mut transport, frame.request_id, ErrorCode::Conflict, "AgencyProxy is already stopping", - ).await?; + ) + .await?; continue; } // This flag and StartRun share the same lock. Once the // acknowledgement is visible, no new provider can pass // the admission gate while existing runs drain. lifecycle.stopping = true; - send_response( - &mut transport, - frame.request_id, - ServerResponse::Accepted, - ).await?; + send_response(&mut transport, frame.request_id, ServerResponse::Accepted) + .await?; drop(lifecycle); - tokio::spawn(async move { + // Detached, as tokio's spawn was: the drain outlives + // this connection, which returns now. + drop(nagoya::spawn(async move { if mode == ShutdownMode::Terminate { registry.cancel_all().await; } registry.wait_until_idle().await; - let _ = request_shutdown.send(true); - }); + request_shutdown.set(); + })); return Ok(()); } - ClientMessage::StartRun { run_id, request, .. } => { - let lifecycle = lifecycle.lock().await; + ClientMessage::StartRun { + run_id, request, .. + } => { + let lifecycle = lifecycle.write().await; if lifecycle.stopping { send_error( &mut transport, frame.request_id, ErrorCode::Conflict, "AgencyProxy is stopping", - ).await?; + ) + .await?; continue; } match registry.start(run_id, *request).await { - Ok(()) => send_response(&mut transport, frame.request_id, ServerResponse::Accepted).await?, - Err(error) => send_runtime_error(&mut transport, frame.request_id, error).await?, + Ok(()) => { + send_response( + &mut transport, + frame.request_id, + ServerResponse::Accepted, + ) + .await? + } + Err(error) => { + send_runtime_error(&mut transport, frame.request_id, error).await? + } } } - ClientMessage::AttachRun { run_id, after_sequence } => { + ClientMessage::AttachRun { + run_id, + after_sequence, + } => { match registry.attach(&run_id, after_sequence).await { Ok(attachment) => { - send_response(&mut transport, frame.request_id, ServerResponse::Run { - run: attachment.snapshot, - }).await?; + send_response( + &mut transport, + frame.request_id, + ServerResponse::Run { + run: attachment.snapshot, + }, + ) + .await?; for event in &attachment.replay { send_server(&mut transport, &event_frame(event)).await?; } - if let Some(previous) = attachments.remove(&run_id) { previous.abort(); } + if let Some(previous) = attachments.remove(&run_id) { + previous.abort(); + } let event_sender = outbound.clone(); let attached_registry = registry.clone(); let attached_run = run_id.clone(); let mut events = attachment.events; - let mut last_sequence = attachment.replay.last() + let mut last_sequence = attachment + .replay + .last() .map_or(after_sequence, |event| event.sequence); - attachments.insert(run_id, tokio::spawn(async move { + let (forward, abort) = abortable(async move { loop { match events.recv().await { Ok(event) => { - if event.sequence <= last_sequence { continue; } + if event.sequence <= last_sequence { + continue; + } last_sequence = event.sequence; - if event_sender.send(event_frame(&event)).is_err() { return; } + if event_sender + .unbounded_send(event_frame(&event)) + .is_err() + { + return; + } } - Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => { - let Ok(replay) = attached_registry.attach(&attached_run, last_sequence).await else { return; }; + Err(broadcast::RecvError::Lagged(_)) => { + let Ok(replay) = attached_registry + .attach(&attached_run, last_sequence) + .await + else { + return; + }; for event in &replay.replay { last_sequence = event.sequence; - if event_sender.send(event_frame(event)).is_err() { return; } + if event_sender + .unbounded_send(event_frame(event)) + .is_err() + { + return; + } } events = replay.events; } - Err(tokio::sync::broadcast::error::RecvError::Closed) => return, + Err(broadcast::RecvError::Closed) => return, } } - })); + }); + // Detached; `abort` is how it is stopped. + drop(nagoya::spawn(forward)); + attachments.insert(run_id, abort); + } + Err(error) => { + send_runtime_error(&mut transport, frame.request_id, error).await? } - Err(error) => send_runtime_error(&mut transport, frame.request_id, error).await?, } } ClientMessage::DetachRun { run_id } => { - if let Some(task) = attachments.remove(&run_id) { task.abort(); } - send_response(&mut transport, frame.request_id, ServerResponse::Accepted).await?; + if let Some(task) = attachments.remove(&run_id) { + task.abort(); + } + send_response(&mut transport, frame.request_id, ServerResponse::Accepted) + .await?; } ClientMessage::InjectMessage { run_id, body, .. } => { match registry.inject(&run_id, &body).await { - Ok(()) => send_response(&mut transport, frame.request_id, ServerResponse::Accepted).await?, - Err(error) => send_runtime_error(&mut transport, frame.request_id, error).await?, + Ok(()) => { + send_response( + &mut transport, + frame.request_id, + ServerResponse::Accepted, + ) + .await? + } + Err(error) => { + send_runtime_error(&mut transport, frame.request_id, error).await? + } } } ClientMessage::CancelRun { run_id, .. } => { match registry.cancel(&run_id).await { - Ok(()) => send_response(&mut transport, frame.request_id, ServerResponse::Accepted).await?, - Err(error) => send_runtime_error(&mut transport, frame.request_id, error).await?, + Ok(()) => { + send_response( + &mut transport, + frame.request_id, + ServerResponse::Accepted, + ) + .await? + } + Err(error) => { + send_runtime_error(&mut transport, frame.request_id, error).await? + } } } - ClientMessage::InterruptSession { provider, session_id, binary, .. } => { - match registry.interrupt_session(&provider, &session_id, binary).await { - Ok(interrupted) => send_response( + ClientMessage::InterruptSession { + provider, + session_id, + binary, + .. + } => { + match registry + .interrupt_session(&provider, &session_id, binary) + .await + { + Ok(interrupted) => { + send_response( + &mut transport, + frame.request_id, + ServerResponse::SessionInterrupted { interrupted }, + ) + .await? + } + Err(error) => { + send_runtime_error(&mut transport, frame.request_id, error).await? + } + } + } + ClientMessage::DecideApproval { + run_id, + approval_id, + decision, + .. + } => match registry.decide(&run_id, &approval_id, decision).await { + Ok(()) => { + send_response( &mut transport, frame.request_id, - ServerResponse::SessionInterrupted { interrupted }, - ).await?, - Err(error) => send_runtime_error(&mut transport, frame.request_id, error).await?, + ServerResponse::Accepted, + ) + .await? } - } - ClientMessage::DecideApproval { run_id, approval_id, decision, .. } => { - match registry.decide(&run_id, &approval_id, decision).await { - Ok(()) => send_response(&mut transport, frame.request_id, ServerResponse::Accepted).await?, - Err(error) => send_runtime_error(&mut transport, frame.request_id, error).await?, + Err(error) => { + send_runtime_error(&mut transport, frame.request_id, error).await? } - } - ClientMessage::AckEvents { run_id, through_sequence } => { - match registry.acknowledge(&run_id, through_sequence).await { - Ok(()) => send_response(&mut transport, frame.request_id, ServerResponse::Accepted).await?, - Err(error) => send_runtime_error(&mut transport, frame.request_id, error).await?, + }, + ClientMessage::AckEvents { + run_id, + through_sequence, + } => match registry.acknowledge(&run_id, through_sequence).await { + Ok(()) => { + send_response( + &mut transport, + frame.request_id, + ServerResponse::Accepted, + ) + .await? } - } + Err(error) => { + send_runtime_error(&mut transport, frame.request_id, error).await? + } + }, ClientMessage::Hello { .. } => { - send_error(&mut transport, frame.request_id, ErrorCode::ProtocolViolation, "hello may only be sent once").await?; + send_error( + &mut transport, + frame.request_id, + ErrorCode::ProtocolViolation, + "hello may only be sent once", + ) + .await?; } } } diff --git a/crates/proxy/src/main.rs b/crates/proxy/src/main.rs index 4f592c5..e96552d 100644 --- a/crates/proxy/src/main.rs +++ b/crates/proxy/src/main.rs @@ -1,9 +1,11 @@ use agency_proxy::{ - ConnectionConfig, ProxyConfig, ProxyServer, RuntimeRegistry, WebSocketConfig, - WebSocketTlsConfig, serve_websocket, + ConnectionConfig, ProxyConfig, ProxyServer, RuntimeRegistry, WebSocketConfig, serve_websocket, }; use clap::Parser; -use std::{path::PathBuf, process::ExitCode}; +use futures::{channel::oneshot, future::select}; +use nagoya::reactor::{Handle, Reactor, block_on_with}; +use nagoya::signal::{Signal, SignalKind}; +use std::{path::PathBuf, pin::pin, process::ExitCode}; #[derive(Debug, Parser)] #[command(name = "agency-proxy", version, about)] @@ -16,11 +18,19 @@ struct Args { socket: Option, } -#[tokio::main] -async fn main() -> ExitCode { +/// The composition root: the one place that owns the process's signals and +/// its reactor, and hands the reactor's handle to everything that registers a +/// descriptor. +/// +/// The order is load-bearing. Signals first, before any thread exists (see +/// `termination`); then the reactor, whose own thread therefore inherits the +/// signal block; then serving. `nagoya::block_on` drives each future on this +/// thread and starts no thread of its own, and reading the config is a plain +/// file read, so nothing before `termination` creates one either. +fn main() -> ExitCode { let args = Args::parse(); let config = match args.config { - Some(path) => match ProxyConfig::read(&path).await { + Some(path) => match nagoya::block_on(ProxyConfig::read(&path)) { Ok(config) => config, Err(error) => { eprintln!("could not start AgencyProxy: {error}"); @@ -35,43 +45,132 @@ async fn main() -> ExitCode { }, }, }; - let registry = RuntimeRegistry::default(); - let result = match config.connection { - ConnectionConfig::Unix { socket } => serve_unix(socket, registry).await, + let stopped = match &config.connection { + ConnectionConfig::Unix { .. } => None, + ConnectionConfig::WebSocket { .. } => match termination() { + Ok(stopped) => Some(stopped), + Err(error) => { + eprintln!("AgencyProxy stopped: could not register SIGTERM and SIGINT: {error}"); + return ExitCode::FAILURE; + } + }, + }; + // Owned here until `main` returns. Every socket and provider process is + // registered on it, and a dropped reactor stops its thread and hangs them. + let reactor = match Reactor::start() { + Ok(reactor) => reactor, + Err(error) => { + eprintln!("AgencyProxy stopped: could not start the I/O reactor: {error}"); + return ExitCode::FAILURE; + } + }; + let result = nagoya::block_on(serve(config.connection, &reactor.handle(), stopped)); + drop(reactor); + match result { + Ok(()) => ExitCode::SUCCESS, + Err(error) => { + eprintln!("AgencyProxy stopped: {error}"); + ExitCode::FAILURE + } + } +} + +/// Resolves on the first SIGTERM or SIGINT. +/// +/// Both are registered before this returns, so a signal that arrives while the +/// server is still binding is not lost to the default action. +/// +/// Called before anything in the process has started a thread, and that order +/// is load-bearing on Linux. There a nagoya `Signal` is a `signalfd`, which +/// only receives a signal that is blocked, and the block it installs covers the +/// calling thread and the threads it creates afterwards but not threads that +/// already exist. A SIGTERM delivered to an older thread would take the +/// default action and kill the process without a drain. So the waiters are +/// made here on the main thread, first, and every thread after them (the +/// reactor, the pool, the WebSocket server) inherits the block. On the BSDs +/// and macOS it is a process-wide handler and the order does not matter. +/// +/// The reactor is a local one, with no thread of its own, for the same reason: +/// starting a threaded reactor to register on would create a thread before the +/// block. A dedicated thread, started after the waiters exist, polls it. +fn termination() -> std::io::Result> { + let reactor = Reactor::local()?; + let handle = reactor.handle(); + let mut terminate = Signal::new(SignalKind::terminate(), &handle)?; + let mut interrupt = Signal::new(SignalKind::interrupt(), &handle)?; + let (stop, stopped) = oneshot::channel(); + std::thread::Builder::new() + .name("agency-proxy-signals".into()) + .spawn(move || { + block_on_with(&reactor, async move { + // Either outcome is a stop, including a failed wait: signals + // can then no longer be observed, and draining now is safer + // than running on unable to hear the operator. + let _ = select(pin!(terminate.recv()), pin!(interrupt.recv())).await; + let _ = stop.send(()); + // Never returns, so the waiters are never dropped. Dropping a + // nagoya `Signal` restores the default disposition, and a + // second SIGTERM during the drain would then kill the process. + // tokio never restored it and later signals were no-ops; + // keeping the waiters alive for the life of the process + // preserves that. Nothing reads them again, so nothing spins. + // The thread does not hold the process open: it ends when + // `main` returns. + std::future::pending::<()>().await; + drop((terminate, interrupt)); + }); + })?; + Ok(stopped) +} + +async fn serve( + connection: ConnectionConfig, + reactor: &Handle, + stopped: Option>, +) -> Result<(), String> { + let registry = RuntimeRegistry::new(reactor.clone()); + match connection { + ConnectionConfig::Unix { socket } => serve_unix(socket, registry, reactor).await, ConnectionConfig::WebSocket { address, authentication_key, allowed_origins, - tls, } => { eprintln!("AgencyProxy MCP/WebSocket listening on {address}"); + // The process owns its signals and tells the server when to stop. + // The server polls `stop` on its own thread under its own reactor, + // so the signal is waited for elsewhere and forwarded through a + // oneshot, which needs no runtime to await. serve_websocket( registry, WebSocketConfig { address, authentication_key, allowed_origins, - tls: tls.map(|tls| WebSocketTlsConfig { - certificates: tls.certificates, - private_key: tls.private_key, - }), + }, + async move { + if let Some(stopped) = stopped { + let _ = stopped.await; + } else { + // `main` registers the signals for every WebSocket + // configuration, so this is not reached; without them + // the server runs until the process is killed. + std::future::pending::<()>().await; + } }, ) .await .map_err(|error| error.to_string()) } - }; - match result { - Ok(()) => ExitCode::SUCCESS, - Err(error) => { - eprintln!("AgencyProxy stopped: {error}"); - ExitCode::FAILURE - } } } -async fn serve_unix(socket: PathBuf, registry: RuntimeRegistry) -> Result<(), String> { - let server = ProxyServer::bind_with_registry(&socket, registry) +async fn serve_unix( + socket: PathBuf, + registry: RuntimeRegistry, + reactor: &Handle, +) -> Result<(), String> { + let server = ProxyServer::bind_with_registry(&socket, registry, reactor) .await .map_err(|error| error.to_string())?; eprintln!( diff --git a/crates/proxy/src/runtime.rs b/crates/proxy/src/runtime.rs index c4ad994..e2a641b 100644 --- a/crates/proxy/src/runtime.rs +++ b/crates/proxy/src/runtime.rs @@ -1,14 +1,24 @@ +use crate::{broadcast, flag::Flag}; use agency_proxy_protocol::{ApprovalDecision, RunEvent, RunId, RunRequest, RunSnapshot, RunState}; use agent_abstraction::{ Agent, AuthState, AuthStatus, Decision, Event, Permission, Probe, Request, VersionStatus, interrupt, }; +use futures::{ + channel::oneshot, + future::{Either, select}, +}; +use nagoya::reactor::Handle; +use nagoya::sync::{Notify, RwLock}; use std::{ collections::{BTreeMap, VecDeque}, - sync::Arc, + pin::pin, + sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }, }; use thiserror::Error; -use tokio::sync::{RwLock, broadcast, oneshot, watch}; const MAX_REPLAY_EVENTS: usize = 2_048; @@ -27,63 +37,71 @@ struct LiveRun { events: broadcast::Sender, control: agent_abstraction::RunControl, cancel: Option>, - completed: watch::Sender, + completed: Arc, } +/// Owns provider runs across connection transports. +/// +/// Under tokio the registry captured the daemon runtime's handle so that a +/// WebSocket shard or a disconnected client's runtime could not become the +/// accidental owner of provider tasks. Tasks now go to nagoya's shared pool, +/// which belongs to no connection. What the registry holds instead is the +/// reactor its provider processes are registered on, given to it by whoever +/// owns that reactor. There is no `Default`: a registry cannot conjure a +/// reactor without owning one. #[derive(Clone, Debug)] pub struct RuntimeRegistry { runs: Arc>>, activity: RunActivity, - executor: tokio::runtime::Handle, + reactor: Handle, } -#[derive(Clone, Debug)] +/// How many accepted runs have not reached a terminal state. +/// +/// This was a `tokio::sync::watch::Sender`. Only the count and a wake +/// on change were used, so it is an atomic and a `Notify`. +#[derive(Clone, Debug, Default)] struct RunActivity { - active: watch::Sender, + inner: Arc, } -impl Default for RunActivity { - fn default() -> Self { - let (active, _) = watch::channel(0); - Self { active } - } +#[derive(Debug, Default)] +struct ActivityState { + active: AtomicUsize, + changed: Notify, } impl RunActivity { fn started(&self) { - self.active.send_modify(|active| *active += 1); + self.inner.active.fetch_add(1, Ordering::SeqCst); + self.inner.changed.notify_waiters(); } fn finished(&self) { - self.active.send_modify(|active| { - debug_assert!(*active > 0, "a run finished without being active"); - *active = active.saturating_sub(1); - }); + let (Ok(previous) | Err(previous)) = + self.inner + .active + .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |active| { + Some(active.saturating_sub(1)) + }); + debug_assert!(previous > 0, "a run finished without being active"); + self.inner.changed.notify_waiters(); } fn count(&self) -> usize { - *self.active.borrow() + self.inner.active.load(Ordering::SeqCst) } async fn wait_until_idle(&self) { - let mut active = self.active.subscribe(); - while *active.borrow_and_update() > 0 { - if active.changed().await.is_err() { - break; + loop { + // Created before the count is read: `Notify` snapshots its + // broadcast generation here, so a run finishing between the read + // and the await still wakes this waiter. + let changed = self.inner.changed.notified(); + if self.count() == 0 { + return; } - } - } -} - -impl Default for RuntimeRegistry { - fn default() -> Self { - Self { - runs: Arc::default(), - activity: RunActivity::default(), - // The registry owns provider tasks across connection transports. - // Capturing the daemon runtime here prevents a WebSocket shard or - // disconnected client runtime from becoming their accidental owner. - executor: tokio::runtime::Handle::current(), + changed.await; } } } @@ -113,7 +131,33 @@ pub enum RuntimeError { } impl RuntimeRegistry { + /// An empty registry whose provider processes are registered on + /// `reactor`. The caller owns the reactor and must keep it running for as + /// long as any run it started may still be live. + #[must_use] + pub fn new(reactor: Handle) -> Self { + Self { + runs: Arc::default(), + activity: RunActivity::default(), + reactor, + } + } + + /// Run `future` on the pool that owns this registry's provider tasks. + /// + /// For work started from a transport whose own executor is not the pool, + /// such as an endpoint-libs WebSocket handler, which is polled in place on + /// that server's reactor thread and has no spawner of its own. + pub(crate) fn spawn(&self, future: F) -> nagoya::JoinHandle + where + F: std::future::Future + Send + 'static, + F::Output: Send + 'static, + { + nagoya::spawn(future) + } + pub async fn account_usage(&self) -> Vec { + let reactor = &self.reactor; futures::future::join_all( [Agent::Claude, Agent::Codex, Agent::Copilot, Agent::Grok].map(|agent| async move { let provider = agent_name(agent).to_string(); @@ -125,7 +169,7 @@ impl RuntimeRegistry { error: None, }; } - match agent.account_usage().await { + match agent.account_usage(reactor).await { Ok(usage) => agency_proxy_protocol::ProviderAccountUsage { provider, supported: true, @@ -145,10 +189,11 @@ impl RuntimeRegistry { } pub async fn probe_providers(&self) -> Vec { + let reactor = &self.reactor; futures::future::join_all( [Agent::Claude, Agent::Codex, Agent::Copilot, Agent::Grok].map(|agent| async move { - let probe = Probe::run(agent).await; - let auth = AuthStatus::check(agent).await; + let probe = Probe::run(agent, reactor).await; + let auth = AuthStatus::check(agent, reactor).await; let installed = !matches!(probe, Err(agent_abstraction::Error::NotInstalled { .. })); let probe = probe.ok(); @@ -201,10 +246,13 @@ impl RuntimeRegistry { pub async fn start(&self, run_id: RunId, spec: RunRequest) -> Result<(), RuntimeError> { let registry = self.clone(); - self.executor - .spawn(async move { registry.start_owned(run_id, spec).await }) + // `None` is nagoya's JoinError: the pool went away before the task + // could finish. A panic inside is rethrown here rather than returned, + // where tokio returned it as an error, but provider starts do not + // panic by design and a panic is a bug either way. + self.spawn(async move { registry.start_owned(run_id, spec).await }) .await - .map_err(|error| RuntimeError::Start(format!("proxy runtime stopped: {error}")))? + .ok_or_else(|| RuntimeError::Start("proxy runtime stopped".into()))? } async fn start_owned(&self, run_id: RunId, spec: RunRequest) -> Result<(), RuntimeError> { @@ -212,12 +260,12 @@ impl RuntimeRegistry { return Err(RuntimeError::Conflict); } let request = build_request(spec.clone())?; - let mut run = agent_abstraction::stream(&request) + let mut run = agent_abstraction::stream(&request, &self.reactor) .map_err(|error| RuntimeError::Start(error.to_string()))?; let control = run.control(); let (events, _) = broadcast::channel(256); let (cancel, mut cancelled) = oneshot::channel(); - let (completed, _) = watch::channel(false); + let completed = Arc::new(Flag::default()); let snapshot = RunSnapshot { run_id: run_id.clone(), state: RunState::Starting, @@ -250,31 +298,36 @@ impl RuntimeRegistry { } let registry = self.clone(); - self.executor.spawn(async move { + // Detached, as tokio's spawn was: the run outlives the request that + // started it and ends on its own terminal event or on cancellation. + drop(self.spawn(async move { loop { - tokio::select! { - biased; - _ = &mut cancelled => { - // Cooperative cancellation gives interactive providers - // their protocol-level interrupt before the abstraction's - // bounded process-group fallback. Dropping this handle - // skipped Codex `turn/interrupt`, leaving the server-owned - // turn alive after AgencyProxy reported it canceled. - let _ = run.cancel().await; - registry - .publish_error( - &run_id, - "the run was canceled".into(), - RunState::Canceled, - ) - .await; - return; - } - event = run.recv() => { - let Some(event) = event else { break }; - registry.publish_provider(&run_id, event).await; - } - } + // `select` polls its first argument first, which is the + // `biased;` ordering the tokio version asked for: a pending + // cancellation wins over an event that is ready at the same + // time. The cancellation receiver is borrowed, so losing to an + // event leaves it armed for the next turn; the `recv` future is + // dropped at the end of this statement either way, which frees + // `run` for `cancel` below. A dropped sender resolves the + // receiver too, exactly as `&mut cancelled` did under tokio. + let event = match select(&mut cancelled, pin!(run.recv())).await { + Either::Left(_) => None, + Either::Right((event, _)) => Some(event), + }; + let Some(event) = event else { + // Cooperative cancellation gives interactive providers + // their protocol-level interrupt before the abstraction's + // bounded process-group fallback. Dropping this handle + // skipped Codex `turn/interrupt`, leaving the server-owned + // turn alive after AgencyProxy reported it canceled. + let _ = run.cancel().await; + registry + .publish_error(&run_id, "the run was canceled".into(), RunState::Canceled) + .await; + return; + }; + let Some(event) = event else { break }; + registry.publish_provider(&run_id, event).await; } match run.finish().await { Ok(outcome) => { @@ -291,7 +344,7 @@ impl RuntimeRegistry { .await } } - }); + })); Ok(()) } @@ -375,14 +428,14 @@ impl RuntimeRegistry { } pub async fn cancel(&self, run_id: &RunId) -> Result<(), RuntimeError> { - // Subscribe before delivering cancellation. A fast provider can emit - // its terminal event in the same scheduling turn; installing the - // receiver first makes the acknowledgement race-free. + // Take the completion flag before delivering cancellation. A fast + // provider can emit its terminal event in the same scheduling turn; + // the flag stays set once set, so the acknowledgement is race-free. let (cancel, completed) = { let mut runs = self.runs.write().await; let run = runs.get_mut(run_id).ok_or(RuntimeError::NotFound)?; let cancel = run.cancel.take().ok_or(RuntimeError::Conflict)?; - (cancel, run.completed.subscribe()) + (cancel, Arc::clone(&run.completed)) }; let _ = cancel.send(()); wait_until_completed(completed).await; @@ -407,7 +460,7 @@ impl RuntimeRegistry { if let Some(binary) = binary.filter(|value| !value.is_empty()) { request = request.bin(binary); } - interrupt(&request) + interrupt(&request, &self.reactor) .await .map_err(|error| RuntimeError::Control(error.to_string())) } @@ -562,18 +615,14 @@ impl RuntimeRegistry { push_journal(&mut run.journal, &mut run.replay_floor, event.clone()); let _ = run.events.send(event); if finished { - run.completed.send_replace(true); + run.completed.set(); self.activity.finished(); } } } -async fn wait_until_completed(mut completed: watch::Receiver) { - while !*completed.borrow_and_update() { - if completed.changed().await.is_err() { - break; - } - } +async fn wait_until_completed(completed: Arc) { + completed.wait().await; } fn is_active_state(state: &RunState) -> bool { @@ -703,48 +752,63 @@ mod tests { assert_eq!(journal.front().map(|event| event.sequence), Some(4)); } - #[tokio::test] - async fn activity_waits_for_the_last_run_without_polling() { - let activity = RunActivity::default(); - activity.started(); - activity.started(); + /// Long enough for the shared pool to have polled a freshly spawned task. + /// + /// The tokio tests ran on a current-thread runtime, where one `yield_now` + /// guaranteed the spawned waiter had been polled. nagoya's pool runs it on + /// another thread, so a yield on this one proves nothing about it; a short + /// sleep gives it time to park. The assertions only ever check that the + /// waiter has *not* finished, so a slow pool cannot make them flaky. + const SETTLE: std::time::Duration = std::time::Duration::from_millis(20); - let waiter = tokio::spawn({ - let activity = activity.clone(); - async move { activity.wait_until_idle().await } - }); - tokio::task::yield_now().await; - assert!(!waiter.is_finished()); + #[test] + fn activity_waits_for_the_last_run_without_polling() { + nagoya::block_on(async { + let activity = RunActivity::default(); + activity.started(); + activity.started(); + + let waiter = nagoya::spawn({ + let activity = activity.clone(); + async move { activity.wait_until_idle().await } + }); + nagoya::sleep(SETTLE).await; + assert!(!waiter.is_finished()); - activity.finished(); - tokio::task::yield_now().await; - assert!(!waiter.is_finished()); + activity.finished(); + nagoya::sleep(SETTLE).await; + assert!(!waiter.is_finished()); - activity.finished(); - waiter.await.unwrap(); - assert_eq!(activity.count(), 0); + activity.finished(); + waiter.await.expect("the waiter should run to completion"); + assert_eq!(activity.count(), 0); + }); } - #[tokio::test] - async fn activity_wait_returns_immediately_when_idle() { - RunActivity::default().wait_until_idle().await; + #[test] + fn activity_wait_returns_immediately_when_idle() { + nagoya::block_on(RunActivity::default().wait_until_idle()); } - #[tokio::test] - async fn completion_wait_observes_a_signal_without_polling() { - let (completed, receiver) = watch::channel(false); - let waiter = tokio::spawn(wait_until_completed(receiver)); - tokio::task::yield_now().await; - assert!(!waiter.is_finished()); - - completed.send_replace(true); - waiter.await.unwrap(); + #[test] + fn completion_wait_observes_a_signal_without_polling() { + nagoya::block_on(async { + let completed = Arc::new(Flag::default()); + let waiter = nagoya::spawn(wait_until_completed(Arc::clone(&completed))); + nagoya::sleep(SETTLE).await; + assert!(!waiter.is_finished()); + + completed.set(); + waiter.await.expect("the waiter should run to completion"); + }); } - #[tokio::test] - async fn completion_wait_returns_immediately_after_the_signal() { - let (completed, receiver) = watch::channel(false); - completed.send_replace(true); - wait_until_completed(receiver).await; + #[test] + fn completion_wait_returns_immediately_after_the_signal() { + nagoya::block_on(async { + let completed = Arc::new(Flag::default()); + completed.set(); + wait_until_completed(completed).await; + }); } } diff --git a/crates/proxy/src/web.rs b/crates/proxy/src/web.rs index daa8d21..2cdd61d 100644 --- a/crates/proxy/src/web.rs +++ b/crates/proxy/src/web.rs @@ -17,12 +17,15 @@ use endpoint_libs::{ }, model::TypeRegistry, }; -use futures::{FutureExt, future::LocalBoxFuture}; +use futures::{ + FutureExt, + channel::oneshot, + future::{AbortHandle, LocalBoxFuture, abortable}, +}; use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, net::SocketAddr, - path::PathBuf, sync::{ Arc, Mutex, atomic::{AtomicU64, Ordering}, @@ -36,13 +39,6 @@ pub struct WebSocketConfig { pub address: SocketAddr, pub authentication_key: String, pub allowed_origins: Vec, - pub tls: Option, -} - -#[derive(Clone, Debug)] -pub struct WebSocketTlsConfig { - pub certificates: Vec, - pub private_key: PathBuf, } struct Authentication { @@ -71,21 +67,67 @@ impl AuthController for Authentication { } } -pub async fn serve_websocket( +/// Serve MCP over WebSocket until `stop` resolves, then drain accepted runs. +/// +/// `stop` is the caller's, not a signal this function takes. endpoint-libs' +/// `listen` would claim SIGTERM and SIGINT for the process, and a second server +/// in the same process then fails with `EBUSY`; signals belong to `main`. `stop` +/// is polled on the server's own thread, under that server's own reactor, so it +/// must not need any other runtime: a `futures` oneshot receiver is the +/// intended shape. +pub async fn serve_websocket( registry: RuntimeRegistry, config: WebSocketConfig, -) -> eyre::Result<()> { + stop: S, +) -> eyre::Result<()> +where + S: std::future::Future + Send + 'static, +{ let drain_registry = registry.clone(); - let (insecure, pub_certs, priv_key) = match config.tls { - Some(tls) => (false, Some(tls.certificates), Some(tls.private_key)), - None => (true, None, None), - }; + // The server blocks the thread it runs on: it owns a reactor and polls it + // until `stop`. On a pool worker that would hold the worker for the life + // of the process, and inside `nagoya::block_on`, which `main` and every + // test use, it would hold the only thread. It gets a thread of its own, + // and this future waits for its answer without blocking. + let (answer, answered) = oneshot::channel(); + std::thread::Builder::new() + .name("agency-proxy-websocket".into()) + .spawn(move || { + let _ = answer.send(listen_websocket(registry, config, stop)); + })?; + let result = answered.await.unwrap_or_else(|_| { + Err(eyre::eyre!( + "the WebSocket server thread ended without a result" + )) + }); + let active = drain_registry.active_count().await; + if active > 0 { + eprintln!("AgencyProxy WebSocket admission closed; draining {active} active run(s)"); + } + // endpoint-libs closes admission before returning from `listen` on TERM. + // Keep the process alive until every provider already accepted by this + // registry settles, matching the Unix transport's drain restart contract. + drain_registry.wait_until_idle().await; + if active > 0 { + eprintln!("AgencyProxy WebSocket runs drained"); + } + result +} + +fn listen_websocket( + registry: RuntimeRegistry, + config: WebSocketConfig, + stop: S, +) -> eyre::Result<()> +where + S: std::future::Future + 'static, +{ + let drain_registry = registry.clone(); + // Plain ws:// only. The listener is loopback by validation, and + // endpoint-libs 3 refuses certificates outright: TLS terminates at the edge. let mut server = WebsocketServer::new(WsServerConfig { name: "agency-proxy".into(), address: config.address.to_string(), - insecure, - pub_certs, - priv_key, allow_cors_urls: Arc::new(Some(config.allowed_origins)), drop_conn_on_buffer_full: true, mcp_only: true, @@ -117,26 +159,20 @@ pub async fn serve_websocket( version: env!("CARGO_PKG_VERSION").into(), }, )?; - let result = server.listen().await; - let active = drain_registry.active_count().await; - if active > 0 { - eprintln!("AgencyProxy WebSocket admission closed; draining {active} active run(s)"); - } - // endpoint-libs closes admission before returning from `listen` on TERM. - // Keep the process alive until every provider already accepted by this - // registry settles, matching the Unix transport's drain restart contract. - drain_registry.wait_until_idle().await; - if active > 0 { - eprintln!("AgencyProxy WebSocket runs drained"); - } - result + server.listen_until(stop) } type SubscriptionKey = (ConnectionId, String); +/// Live run-event forwarders, one per connection and run. +/// +/// The handles are `futures` abort handles. tokio's `AbortHandle` came from +/// the spawned task; nagoya's `JoinHandle` has no detachable abort handle, so +/// the forwarder is wrapped in `abortable` before it is spawned. Aborting stops +/// it at its next suspension point, as tokio's did. #[derive(Clone, Default)] struct RunSubscriptions { - active: Arc>>, + active: Arc>>, next_generation: Arc, } @@ -145,7 +181,7 @@ impl RunSubscriptions { self.next_generation.fetch_add(1, Ordering::Relaxed) + 1 } - fn insert(&self, key: SubscriptionKey, generation: u64, handle: tokio::task::AbortHandle) { + fn insert(&self, key: SubscriptionKey, generation: u64, handle: AbortHandle) { if let Ok(mut active) = self.active.lock() && let Some((_, previous)) = active.insert(key, (generation, handle)) { @@ -389,7 +425,11 @@ impl RequestHandler for MethodSubscribeRun { let registry = self.registry.clone(); let toolbox = TOOLBOX.with(Arc::clone); let connection_id = ctx.connection_id; - let task = tokio::task::spawn_local(async move { + // On the registry's pool, not the connection's thread. endpoint-libs 3 + // polls a handler in place on the connection task and has no spawner. + // The forwarder needs nothing thread-bound: the toolbox is + // `Send + Sync`. + let (forward, abort) = abortable(async move { forward_run_events( registry, toolbox, @@ -401,9 +441,13 @@ impl RequestHandler for MethodSubscribeRun { .await; subscriptions.remove_if_current(&cleanup_key, generation); }); - self.subscriptions - .insert(key, generation, task.abort_handle()); - drop(task); + // Registered before the forwarder is spawned. The tokio version could + // only take the abort handle from the spawned task, so a forwarder + // that finished at once ran its cleanup before it was registered and + // left a stale entry behind until disconnect; this order cannot. + self.subscriptions.insert(key, generation, abort); + // Detached; the abort handle is how it is stopped. + drop(self.registry.spawn(forward)); Ok(SubscribeRunResponse { run: attachment.snapshot, @@ -418,7 +462,7 @@ async fn forward_run_events( connection_id: ConnectionId, run_id: RunId, mut last_sequence: u64, - mut events: tokio::sync::broadcast::Receiver, + mut events: crate::broadcast::Receiver, ) { loop { match events.recv().await { @@ -431,7 +475,7 @@ async fn forward_run_events( return; } } - Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => { + Err(crate::broadcast::RecvError::Lagged(_)) => { let Ok(attachment) = registry.attach(&run_id, last_sequence).await else { return; }; @@ -446,7 +490,7 @@ async fn forward_run_events( } events = attachment.events; } - Err(tokio::sync::broadcast::error::RecvError::Closed) => return, + Err(crate::broadcast::RecvError::Closed) => return, } } } diff --git a/crates/proxy/tests/local_socket.rs b/crates/proxy/tests/local_socket.rs index 374d3c7..743bc92 100644 --- a/crates/proxy/tests/local_socket.rs +++ b/crates/proxy/tests/local_socket.rs @@ -4,558 +4,611 @@ use agency_proxy_protocol::{ ClientFrame, ClientMessage, ErrorCode, MAX_FRAME_BYTES, RunEvent, RunId, RunRequest, ServerFrame, ServerResponse, ShutdownMode, }; -use endpoint_libs::libs::ws::{WireMessage, transport::framed::framed_json_with_max_frame}; +// The neutral framing takes a `futures_io` stream, and `NagoyaStream` presents +// a nagoya socket as one directly, so there is no compat layer in between. +use endpoint_libs::libs::ws::{ + WireMessage, + transport::{framed::framed_json_neutral_with_max_frame, nagoya::NagoyaStream}, +}; use futures::{SinkExt, StreamExt}; -use std::{collections::BTreeMap, os::unix::fs::PermissionsExt, time::Duration}; +use nagoya::net::TcpStream; +use nagoya::reactor::{Addr, Reactor}; +use std::{ + collections::BTreeMap, + os::unix::{ffi::OsStrExt, fs::PermissionsExt}, + time::Duration, +}; use tempfile::tempdir; -use tokio::net::UnixStream; -#[tokio::test] -async fn hello_then_list_runs_uses_the_versioned_local_protocol() { - let dir = tempdir().expect("temp dir should exist"); - let socket = dir.path().join("runtime/agent.sock"); - let server = ProxyServer::bind(&socket) - .await - .expect("server should bind"); - assert_eq!( - std::fs::metadata(socket.parent().expect("socket has parent")) - .expect("runtime dir should exist") - .permissions() - .mode() - & 0o777, - 0o700 - ); - assert_eq!( - std::fs::metadata(&socket) - .expect("socket should exist") - .permissions() - .mode() - & 0o777, - 0o600 - ); - let task = tokio::spawn(server.serve()); - let client = Client::connect(&socket) - .await - .expect("client should connect and negotiate"); - assert_eq!( - client - .request(ClientMessage::ListRuns) +// Every test starts its own reactor and keeps it for the whole test: the +// server's listener, its connections, the clients and the provider processes +// are all registered on it. `nagoya::block_on` polls no reactor, so a threaded +// one is what keeps those sockets moving while the test awaits. + +#[test] +fn hello_then_list_runs_uses_the_versioned_local_protocol() { + let reactor = Reactor::start().expect("test reactor should start"); + let handle = reactor.handle(); + nagoya::block_on(async { + let dir = tempdir().expect("temp dir should exist"); + let socket = dir.path().join("runtime/agent.sock"); + let server = ProxyServer::bind(&socket, &handle) + .await + .expect("server should bind"); + assert_eq!( + std::fs::metadata(socket.parent().expect("socket has parent")) + .expect("runtime dir should exist") + .permissions() + .mode() + & 0o777, + 0o700 + ); + assert_eq!( + std::fs::metadata(&socket) + .expect("socket should exist") + .permissions() + .mode() + & 0o777, + 0o600 + ); + let task = nagoya::spawn(server.serve()); + let client = Client::connect(&socket, &handle) .await - .expect("list should succeed"), - ServerResponse::Runs { runs: Vec::new() } - ); - task.abort(); + .expect("client should connect and negotiate"); + assert_eq!( + client + .request(ClientMessage::ListRuns) + .await + .expect("list should succeed"), + ServerResponse::Runs { runs: Vec::new() } + ); + task.cancel(); + }); } -#[tokio::test] -async fn idle_daemon_accepts_a_graceful_shutdown() { - let dir = tempdir().expect("temp dir should exist"); - let socket = dir.path().join("agent.sock"); - let server = ProxyServer::bind(&socket) - .await - .expect("server should bind"); - let task = tokio::spawn(server.serve()); - let client = Client::connect(&socket) - .await - .expect("client should connect"); +#[test] +fn idle_daemon_accepts_a_graceful_shutdown() { + let reactor = Reactor::start().expect("test reactor should start"); + let handle = reactor.handle(); + nagoya::block_on(async { + let dir = tempdir().expect("temp dir should exist"); + let socket = dir.path().join("agent.sock"); + let server = ProxyServer::bind(&socket, &handle) + .await + .expect("server should bind"); + let task = nagoya::spawn(server.serve()); + let client = Client::connect(&socket, &handle) + .await + .expect("client should connect"); - assert_eq!( - client - .request(ClientMessage::ShutdownIfIdle) + assert_eq!( + client + .request(ClientMessage::ShutdownIfIdle) + .await + .expect("shutdown should answer"), + ServerResponse::Accepted + ); + nagoya::timeout(Duration::from_secs(2), task) .await - .expect("shutdown should answer"), - ServerResponse::Accepted - ); - tokio::time::timeout(Duration::from_secs(2), task) - .await - .expect("server should stop promptly") - .expect("server task should join") - .expect("server should stop cleanly"); - assert!(!socket.exists(), "graceful shutdown removes its socket"); + .expect("server should stop promptly") + .expect("server task should join") + .expect("server should stop cleanly"); + assert!(!socket.exists(), "graceful shutdown removes its socket"); + }); } -#[tokio::test] -async fn shutdown_refuses_to_interrupt_an_active_run() { - let dir = tempdir().expect("temp dir should exist"); - let binary = dir.path().join("slow-claude"); - std::fs::write( - &binary, - r#"#!/bin/sh +#[test] +fn shutdown_refuses_to_interrupt_an_active_run() { + let reactor = Reactor::start().expect("test reactor should start"); + let handle = reactor.handle(); + nagoya::block_on(async { + let dir = tempdir().expect("temp dir should exist"); + let binary = dir.path().join("slow-claude"); + std::fs::write( + &binary, + r#"#!/bin/sh sleep 2 printf '%s\n' '{"type":"result","subtype":"success","is_error":false,"result":"done","session_id":"proxy-session","usage":{"input_tokens":1,"output_tokens":1}}' "#, - ) - .expect("fake provider should write"); - std::fs::set_permissions(&binary, std::fs::Permissions::from_mode(0o700)) - .expect("fake provider should be executable"); + ) + .expect("fake provider should write"); + std::fs::set_permissions(&binary, std::fs::Permissions::from_mode(0o700)) + .expect("fake provider should be executable"); - let socket = dir.path().join("agent.sock"); - let server = ProxyServer::bind(&socket) - .await - .expect("server should bind"); - let task = tokio::spawn(server.serve()); - let client = Client::connect(&socket) - .await - .expect("client should connect"); - let run_id = RunId("active".into()); - assert_eq!( - client - .request(ClientMessage::StartRun { - run_id: run_id.clone(), - request: Box::new(RunRequest { - provider: "claude".into(), - model: String::new(), - prompt: "test".into(), - is_command: false, - system: None, - permission: "read_only".into(), - effort: None, - extra_thinking: None, - approvals: false, - interactive: false, - workspace_roots: vec![dir.path().to_string_lossy().into_owned()], - resume_session_id: None, - binary: Some(binary.to_string_lossy().into_owned()), - environment: BTreeMap::new(), - unchecked_args: Vec::new(), - metadata: BTreeMap::new(), - }), - idempotency_key: "start-active".into(), - }) + let socket = dir.path().join("agent.sock"); + let server = ProxyServer::bind(&socket, &handle) .await - .expect("start should answer"), - ServerResponse::Accepted - ); - - assert!(matches!( - client - .request(ClientMessage::ShutdownIfIdle) + .expect("server should bind"); + let task = nagoya::spawn(server.serve()); + let client = Client::connect(&socket, &handle) .await - .expect("shutdown refusal should answer"), - ServerResponse::Error { - code: ErrorCode::Conflict, - message, - } if message.contains("1 active run") - )); - assert!(socket.exists(), "refused shutdown keeps serving"); + .expect("client should connect"); + let run_id = RunId("active".into()); + assert_eq!( + client + .request(ClientMessage::StartRun { + run_id: run_id.clone(), + request: Box::new(RunRequest { + provider: "claude".into(), + model: String::new(), + prompt: "test".into(), + is_command: false, + system: None, + permission: "read_only".into(), + effort: None, + extra_thinking: None, + approvals: false, + interactive: false, + workspace_roots: vec![dir.path().to_string_lossy().into_owned()], + resume_session_id: None, + binary: Some(binary.to_string_lossy().into_owned()), + environment: BTreeMap::new(), + unchecked_args: Vec::new(), + metadata: BTreeMap::new(), + }), + idempotency_key: "start-active".into(), + }) + .await + .expect("start should answer"), + ServerResponse::Accepted + ); - task.abort(); + assert!(matches!( + client + .request(ClientMessage::ShutdownIfIdle) + .await + .expect("shutdown refusal should answer"), + ServerResponse::Error { + code: ErrorCode::Conflict, + message, + } if message.contains("1 active run") + )); + assert!(socket.exists(), "refused shutdown keeps serving"); + + task.cancel(); + }); } -#[tokio::test] -async fn cancel_acknowledges_only_after_an_uncooperative_provider_is_terminal() { - let dir = tempdir().expect("temp dir should exist"); - let binary = dir.path().join("stubborn-claude"); - std::fs::write(&binary, "#!/bin/sh\ntrap '' TERM\nsleep 30\n") - .expect("fake provider should write"); - std::fs::set_permissions(&binary, std::fs::Permissions::from_mode(0o700)) - .expect("fake provider should be executable"); +#[test] +fn cancel_acknowledges_only_after_an_uncooperative_provider_is_terminal() { + let reactor = Reactor::start().expect("test reactor should start"); + let handle = reactor.handle(); + nagoya::block_on(async { + let dir = tempdir().expect("temp dir should exist"); + let binary = dir.path().join("stubborn-claude"); + std::fs::write(&binary, "#!/bin/sh\ntrap '' TERM\nsleep 30\n") + .expect("fake provider should write"); + std::fs::set_permissions(&binary, std::fs::Permissions::from_mode(0o700)) + .expect("fake provider should be executable"); - let socket = dir.path().join("agent.sock"); - let server = ProxyServer::bind(&socket) - .await - .expect("server should bind"); - let task = tokio::spawn(server.serve()); - let client = Client::connect(&socket) - .await - .expect("client should connect"); - let mut events = client.subscribe(); - let run_id = RunId("stubborn".into()); - assert_eq!( - client - .request(ClientMessage::StartRun { - run_id: run_id.clone(), - request: Box::new(RunRequest { - provider: "claude".into(), - model: String::new(), - prompt: "test".into(), - is_command: false, - system: None, - permission: "read_only".into(), - effort: None, - extra_thinking: None, - approvals: false, - interactive: false, - workspace_roots: vec![dir.path().to_string_lossy().into_owned()], - resume_session_id: None, - binary: Some(binary.to_string_lossy().into_owned()), - environment: BTreeMap::new(), - unchecked_args: Vec::new(), - metadata: BTreeMap::new(), - }), - idempotency_key: "start-stubborn".into(), - }) + let socket = dir.path().join("agent.sock"); + let server = ProxyServer::bind(&socket, &handle) .await - .expect("start should answer"), - ServerResponse::Accepted - ); - assert!(matches!( - client - .request(ClientMessage::AttachRun { - run_id: run_id.clone(), - after_sequence: 0, - }) + .expect("server should bind"); + let task = nagoya::spawn(server.serve()); + let client = Client::connect(&socket, &handle) .await - .expect("attach should answer"), - ServerResponse::Run { .. } - )); + .expect("client should connect"); + let mut events = client.subscribe(); + let run_id = RunId("stubborn".into()); + assert_eq!( + client + .request(ClientMessage::StartRun { + run_id: run_id.clone(), + request: Box::new(RunRequest { + provider: "claude".into(), + model: String::new(), + prompt: "test".into(), + is_command: false, + system: None, + permission: "read_only".into(), + effort: None, + extra_thinking: None, + approvals: false, + interactive: false, + workspace_roots: vec![dir.path().to_string_lossy().into_owned()], + resume_session_id: None, + binary: Some(binary.to_string_lossy().into_owned()), + environment: BTreeMap::new(), + unchecked_args: Vec::new(), + metadata: BTreeMap::new(), + }), + idempotency_key: "start-stubborn".into(), + }) + .await + .expect("start should answer"), + ServerResponse::Accepted + ); + assert!(matches!( + client + .request(ClientMessage::AttachRun { + run_id: run_id.clone(), + after_sequence: 0, + }) + .await + .expect("attach should answer"), + ServerResponse::Run { .. } + )); - assert_eq!( - client - .request(ClientMessage::CancelRun { - run_id: run_id.clone(), - idempotency_key: "cancel-stubborn".into(), - }) - .await - .expect("cancel should answer"), - ServerResponse::Accepted - ); + assert_eq!( + client + .request(ClientMessage::CancelRun { + run_id: run_id.clone(), + idempotency_key: "cancel-stubborn".into(), + }) + .await + .expect("cancel should answer"), + ServerResponse::Accepted + ); - assert!(matches!( - client - .request(ClientMessage::ListRuns) - .await - .expect("list should answer immediately after cancel"), - ServerResponse::Runs { runs } - if runs.iter().any(|run| run.run_id == run_id && run.state == agency_proxy_protocol::RunState::Canceled) - )); + assert!(matches!( + client + .request(ClientMessage::ListRuns) + .await + .expect("list should answer immediately after cancel"), + ServerResponse::Runs { runs } + if runs.iter().any(|run| run.run_id == run_id && run.state == agency_proxy_protocol::RunState::Canceled) + )); - let terminal = tokio::time::timeout(Duration::from_secs(2), async { - loop { - if let ServerFrame::Event { - run_id: event_run_id, - event: RunEvent::Failed(message), - .. - } = events.recv().await.expect("event stream should stay open") - && event_run_id == run_id - { - break message; + let terminal = nagoya::timeout(Duration::from_secs(2), async { + loop { + if let ServerFrame::Event { + run_id: event_run_id, + event: RunEvent::Failed(message), + .. + } = events.recv().await.expect("event stream should stay open") + && event_run_id == run_id + { + break message; + } } - } - }) - .await - .expect("cancel should publish a terminal event promptly"); - assert_eq!(terminal, "the run was canceled"); - task.abort(); + }) + .await + .expect("cancel should publish a terminal event promptly"); + assert_eq!(terminal, "the run was canceled"); + task.cancel(); + }); } -#[tokio::test] -async fn draining_shutdown_rejects_new_runs_and_waits_for_the_active_run() { - let dir = tempdir().expect("temp dir should exist"); - let binary = dir.path().join("slow-claude"); - let release = dir.path().join("release-provider"); - std::fs::write( - &binary, - r#"#!/bin/sh +#[test] +fn draining_shutdown_rejects_new_runs_and_waits_for_the_active_run() { + let reactor = Reactor::start().expect("test reactor should start"); + let handle = reactor.handle(); + nagoya::block_on(async { + let dir = tempdir().expect("temp dir should exist"); + let binary = dir.path().join("slow-claude"); + let release = dir.path().join("release-provider"); + std::fs::write( + &binary, + r#"#!/bin/sh while [ ! -f "$AGENCY_PROXY_TEST_RELEASE" ]; do sleep 0.01; done printf '%s\n' '{"type":"result","subtype":"success","is_error":false,"result":"done","session_id":"proxy-session","usage":{"input_tokens":1,"output_tokens":1}}' "#, - ) - .expect("fake provider should write"); - std::fs::set_permissions(&binary, std::fs::Permissions::from_mode(0o700)) - .expect("fake provider should be executable"); + ) + .expect("fake provider should write"); + std::fs::set_permissions(&binary, std::fs::Permissions::from_mode(0o700)) + .expect("fake provider should be executable"); - let socket = dir.path().join("agent.sock"); - let server = ProxyServer::bind(&socket) - .await - .expect("server should bind"); - let task = tokio::spawn(server.serve()); - let client = Client::connect(&socket) - .await - .expect("client should connect"); - let request = RunRequest { - provider: "claude".into(), - model: String::new(), - prompt: "test".into(), - is_command: false, - system: None, - permission: "read_only".into(), - effort: None, - extra_thinking: None, - approvals: false, - interactive: false, - workspace_roots: vec![dir.path().to_string_lossy().into_owned()], - resume_session_id: None, - binary: Some(binary.to_string_lossy().into_owned()), - environment: BTreeMap::from([( - "AGENCY_PROXY_TEST_RELEASE".into(), - release.to_string_lossy().into_owned(), - )]), - unchecked_args: Vec::new(), - metadata: BTreeMap::new(), - }; - assert_eq!( - client - .request(ClientMessage::StartRun { - run_id: RunId("active".into()), - request: Box::new(request.clone()), - idempotency_key: "start-active".into(), - }) + let socket = dir.path().join("agent.sock"); + let server = ProxyServer::bind(&socket, &handle) .await - .expect("start should answer"), - ServerResponse::Accepted - ); - assert_eq!( - client - .request(ClientMessage::Shutdown { - mode: ShutdownMode::Drain, - }) + .expect("server should bind"); + let task = nagoya::spawn(server.serve()); + let client = Client::connect(&socket, &handle) .await - .expect("drain should answer"), - ServerResponse::Accepted - ); - let late_client = Client::connect(&socket) - .await - .expect("daemon should keep serving while runs drain"); - assert!( - matches!( - late_client + .expect("client should connect"); + let request = RunRequest { + provider: "claude".into(), + model: String::new(), + prompt: "test".into(), + is_command: false, + system: None, + permission: "read_only".into(), + effort: None, + extra_thinking: None, + approvals: false, + interactive: false, + workspace_roots: vec![dir.path().to_string_lossy().into_owned()], + resume_session_id: None, + binary: Some(binary.to_string_lossy().into_owned()), + environment: BTreeMap::from([( + "AGENCY_PROXY_TEST_RELEASE".into(), + release.to_string_lossy().into_owned(), + )]), + unchecked_args: Vec::new(), + metadata: BTreeMap::new(), + }; + assert_eq!( + client .request(ClientMessage::StartRun { - run_id: RunId("too-late".into()), - request: Box::new(request), - idempotency_key: "start-too-late".into(), + run_id: RunId("active".into()), + request: Box::new(request.clone()), + idempotency_key: "start-active".into(), }) .await - .expect("rejected start should answer"), - ServerResponse::Error { - code: ErrorCode::Conflict, - message, - } if message.contains("stopping") - ), - "draining closes admission before acknowledging" - ); - std::fs::write(&release, "release").expect("provider release should write"); - tokio::time::timeout(Duration::from_secs(5), task) - .await - .expect("server should stop after the run drains") - .expect("server task should join") - .expect("server should stop cleanly"); + .expect("start should answer"), + ServerResponse::Accepted + ); + assert_eq!( + client + .request(ClientMessage::Shutdown { + mode: ShutdownMode::Drain, + }) + .await + .expect("drain should answer"), + ServerResponse::Accepted + ); + let late_client = Client::connect(&socket, &handle) + .await + .expect("daemon should keep serving while runs drain"); + assert!( + matches!( + late_client + .request(ClientMessage::StartRun { + run_id: RunId("too-late".into()), + request: Box::new(request), + idempotency_key: "start-too-late".into(), + }) + .await + .expect("rejected start should answer"), + ServerResponse::Error { + code: ErrorCode::Conflict, + message, + } if message.contains("stopping") + ), + "draining closes admission before acknowledging" + ); + std::fs::write(&release, "release").expect("provider release should write"); + nagoya::timeout(Duration::from_secs(5), task) + .await + .expect("server should stop after the run drains") + .expect("server task should join") + .expect("server should stop cleanly"); + }); } -#[tokio::test] -async fn terminating_shutdown_stops_active_runs_before_exit() { - let dir = tempdir().expect("temp dir should exist"); - let binary = dir.path().join("slow-claude"); - std::fs::write(&binary, "#!/bin/sh\nsleep 30\n").expect("fake provider should write"); - std::fs::set_permissions(&binary, std::fs::Permissions::from_mode(0o700)) - .expect("fake provider should be executable"); +#[test] +fn terminating_shutdown_stops_active_runs_before_exit() { + let reactor = Reactor::start().expect("test reactor should start"); + let handle = reactor.handle(); + nagoya::block_on(async { + let dir = tempdir().expect("temp dir should exist"); + let binary = dir.path().join("slow-claude"); + std::fs::write(&binary, "#!/bin/sh\nsleep 30\n").expect("fake provider should write"); + std::fs::set_permissions(&binary, std::fs::Permissions::from_mode(0o700)) + .expect("fake provider should be executable"); - let socket = dir.path().join("agent.sock"); - let server = ProxyServer::bind(&socket) - .await - .expect("server should bind"); - let task = tokio::spawn(server.serve()); - let client = Client::connect(&socket) - .await - .expect("client should connect"); - assert_eq!( - client - .request(ClientMessage::StartRun { - run_id: RunId("active".into()), - request: Box::new(RunRequest { - provider: "claude".into(), - model: String::new(), - prompt: "test".into(), - is_command: false, - system: None, - permission: "read_only".into(), - effort: None, - extra_thinking: None, - approvals: false, - interactive: false, - workspace_roots: vec![dir.path().to_string_lossy().into_owned()], - resume_session_id: None, - binary: Some(binary.to_string_lossy().into_owned()), - environment: BTreeMap::new(), - unchecked_args: Vec::new(), - metadata: BTreeMap::new(), - }), - idempotency_key: "start-active".into(), - }) + let socket = dir.path().join("agent.sock"); + let server = ProxyServer::bind(&socket, &handle) .await - .expect("start should answer"), - ServerResponse::Accepted - ); - assert_eq!( - client - .request(ClientMessage::Shutdown { - mode: ShutdownMode::Terminate, - }) + .expect("server should bind"); + let task = nagoya::spawn(server.serve()); + let client = Client::connect(&socket, &handle) .await - .expect("terminate should answer"), - ServerResponse::Accepted - ); - tokio::time::timeout(Duration::from_secs(2), task) - .await - .expect("termination should not wait for natural completion") - .expect("server task should join") - .expect("server should stop cleanly"); + .expect("client should connect"); + assert_eq!( + client + .request(ClientMessage::StartRun { + run_id: RunId("active".into()), + request: Box::new(RunRequest { + provider: "claude".into(), + model: String::new(), + prompt: "test".into(), + is_command: false, + system: None, + permission: "read_only".into(), + effort: None, + extra_thinking: None, + approvals: false, + interactive: false, + workspace_roots: vec![dir.path().to_string_lossy().into_owned()], + resume_session_id: None, + binary: Some(binary.to_string_lossy().into_owned()), + environment: BTreeMap::new(), + unchecked_args: Vec::new(), + metadata: BTreeMap::new(), + }), + idempotency_key: "start-active".into(), + }) + .await + .expect("start should answer"), + ServerResponse::Accepted + ); + assert_eq!( + client + .request(ClientMessage::Shutdown { + mode: ShutdownMode::Terminate, + }) + .await + .expect("terminate should answer"), + ServerResponse::Accepted + ); + nagoya::timeout(Duration::from_secs(2), task) + .await + .expect("termination should not wait for natural completion") + .expect("server task should join") + .expect("server should stop cleanly"); + }); } -#[tokio::test] -async fn commands_before_hello_are_rejected() { - let dir = tempdir().expect("temp dir should exist"); - let socket = dir.path().join("agent.sock"); - let server = ProxyServer::bind(&socket) - .await - .expect("server should bind"); - let task = tokio::spawn(server.serve()); - let stream = UnixStream::connect(&socket) - .await - .expect("client should connect"); - let mut transport = framed_json_with_max_frame(stream, MAX_FRAME_BYTES + 1); - let frame = ClientFrame { - request_id: 9, - message: ClientMessage::ListRuns, - }; - transport - .send(WireMessage::Text( - serde_json::to_string(&frame) - .expect("frame should encode") - .into(), - )) - .await - .expect("frame should write"); - let response = transport - .next() - .await - .expect("response should exist") - .expect("response should read"); - let response: ServerFrame = serde_json::from_str( - response - .as_text() - .expect("response should be a JSON text frame"), - ) - .expect("response should decode"); - assert!(matches!( - response, - ServerFrame::Response { +#[test] +fn commands_before_hello_are_rejected() { + let reactor = Reactor::start().expect("test reactor should start"); + let handle = reactor.handle(); + nagoya::block_on(async { + let dir = tempdir().expect("temp dir should exist"); + let socket = dir.path().join("agent.sock"); + let server = ProxyServer::bind(&socket, &handle) + .await + .expect("server should bind"); + let task = nagoya::spawn(server.serve()); + let address = Addr::path(socket.as_os_str().as_bytes()).expect("socket path should fit"); + let stream = TcpStream::connect(address, &handle) + .await + .expect("client should connect"); + let mut transport = + framed_json_neutral_with_max_frame(NagoyaStream::new(stream), MAX_FRAME_BYTES + 1); + let frame = ClientFrame { request_id: 9, - response: ServerResponse::Error { - code: ErrorCode::ProtocolViolation, - .. + message: ClientMessage::ListRuns, + }; + transport + .send(WireMessage::Text( + serde_json::to_string(&frame) + .expect("frame should encode") + .into(), + )) + .await + .expect("frame should write"); + let response = transport + .next() + .await + .expect("response should exist") + .expect("response should read"); + let response: ServerFrame = serde_json::from_str( + response + .as_text() + .expect("response should be a JSON text frame"), + ) + .expect("response should decode"); + assert!(matches!( + response, + ServerFrame::Response { + request_id: 9, + response: ServerResponse::Error { + code: ErrorCode::ProtocolViolation, + .. + } } - } - )); - task.abort(); + )); + task.cancel(); + }); } -#[tokio::test] -async fn refuses_to_replace_a_regular_file_at_the_socket_path() { - let dir = tempdir().expect("temp dir should exist"); - let socket = dir.path().join("agent.sock"); - std::fs::write(&socket, b"owner data").expect("fixture should write"); - let error = ProxyServer::bind(&socket) - .await - .expect_err("regular file must be preserved"); - assert!(matches!(error, agency_proxy::Error::UnsafeSocketPath(path) if path == socket)); - assert_eq!( - std::fs::read(&socket).expect("fixture should remain"), - b"owner data" - ); +#[test] +fn refuses_to_replace_a_regular_file_at_the_socket_path() { + let reactor = Reactor::start().expect("test reactor should start"); + let handle = reactor.handle(); + nagoya::block_on(async { + let dir = tempdir().expect("temp dir should exist"); + let socket = dir.path().join("agent.sock"); + std::fs::write(&socket, b"owner data").expect("fixture should write"); + let error = ProxyServer::bind(&socket, &handle) + .await + .expect_err("regular file must be preserved"); + assert!(matches!(error, agency_proxy::Error::UnsafeSocketPath(path) if path == socket)); + assert_eq!( + std::fs::read(&socket).expect("fixture should remain"), + b"owner data" + ); + }); } -#[tokio::test] -async fn provider_run_survives_a_client_disconnect_and_replays_on_attach() { - let dir = tempdir().expect("temp dir should exist"); - let binary = dir.path().join("fake-claude"); - std::fs::write( - &binary, - r#"#!/bin/sh +#[test] +fn provider_run_survives_a_client_disconnect_and_replays_on_attach() { + let reactor = Reactor::start().expect("test reactor should start"); + let handle = reactor.handle(); + nagoya::block_on(async { + let dir = tempdir().expect("temp dir should exist"); + let binary = dir.path().join("fake-claude"); + std::fs::write( + &binary, + r#"#!/bin/sh printf '%s\n' '{"type":"system","subtype":"init","session_id":"proxy-session","model":"fake-model"}' sleep 1 printf '%s\n' '{"type":"assistant","session_id":"proxy-session","message":{"content":[{"type":"text","text":"survived restart"}]}}' printf '%s\n' '{"type":"result","subtype":"success","is_error":false,"result":"survived restart","session_id":"proxy-session","usage":{"input_tokens":1,"output_tokens":2}}' "#, - ) - .expect("fake provider should write"); - std::fs::set_permissions(&binary, std::fs::Permissions::from_mode(0o700)) - .expect("fake provider should be executable"); - - let socket = dir.path().join("agent.sock"); - let server = ProxyServer::bind(&socket) - .await - .expect("server should bind"); - let task = tokio::spawn(server.serve()); + ) + .expect("fake provider should write"); + std::fs::set_permissions(&binary, std::fs::Permissions::from_mode(0o700)) + .expect("fake provider should be executable"); - let first = Client::connect(&socket) - .await - .expect("first client should connect"); - let run_id = RunId("survivor".into()); - assert!(matches!( - first - .request(ClientMessage::StartRun { - run_id: run_id.clone(), - request: Box::new(RunRequest { - provider: "claude".into(), - model: String::new(), - prompt: "test".into(), - is_command: false, - system: None, - permission: "read_only".into(), - effort: None, - extra_thinking: None, - approvals: false, - interactive: false, - workspace_roots: vec![dir.path().to_string_lossy().into_owned()], - resume_session_id: None, - binary: Some(binary.to_string_lossy().into_owned()), - environment: BTreeMap::new(), - unchecked_args: Vec::new(), - metadata: BTreeMap::new(), - }), - idempotency_key: "start-once".into(), - }) + let socket = dir.path().join("agent.sock"); + let server = ProxyServer::bind(&socket, &handle) .await - .expect("start should succeed"), - ServerResponse::Accepted - )); - drop(first); - tokio::time::sleep(Duration::from_millis(100)).await; + .expect("server should bind"); + let task = nagoya::spawn(server.serve()); - let second = Client::connect(&socket) - .await - .expect("replacement client should connect"); - let mut events = second.subscribe(); - assert!(matches!( - second - .request(ClientMessage::AttachRun { - run_id: run_id.clone(), - after_sequence: 0, - }) + let first = Client::connect(&socket, &handle) .await - .expect("attach should succeed"), - ServerResponse::Run { .. } - )); + .expect("first client should connect"); + let run_id = RunId("survivor".into()); + assert!(matches!( + first + .request(ClientMessage::StartRun { + run_id: run_id.clone(), + request: Box::new(RunRequest { + provider: "claude".into(), + model: String::new(), + prompt: "test".into(), + is_command: false, + system: None, + permission: "read_only".into(), + effort: None, + extra_thinking: None, + approvals: false, + interactive: false, + workspace_roots: vec![dir.path().to_string_lossy().into_owned()], + resume_session_id: None, + binary: Some(binary.to_string_lossy().into_owned()), + environment: BTreeMap::new(), + unchecked_args: Vec::new(), + metadata: BTreeMap::new(), + }), + idempotency_key: "start-once".into(), + }) + .await + .expect("start should succeed"), + ServerResponse::Accepted + )); + drop(first); + nagoya::sleep(Duration::from_millis(100)).await; - let mut saw_text = false; - let mut saw_finished = false; - let mut latest = 0; - for _ in 0..8 { - let frame = tokio::time::timeout(Duration::from_secs(5), events.recv()) + let second = Client::connect(&socket, &handle) .await - .expect("proxy should replay promptly") - .expect("replay frame should exist"); - if let ServerFrame::Event { - sequence, event, .. - } = frame - { - latest = sequence; - match event { - RunEvent::Text(text) if text == "survived restart" => saw_text = true, - RunEvent::Finished(_) => { - saw_finished = true; - break; + .expect("replacement client should connect"); + let mut events = second.subscribe(); + assert!(matches!( + second + .request(ClientMessage::AttachRun { + run_id: run_id.clone(), + after_sequence: 0, + }) + .await + .expect("attach should succeed"), + ServerResponse::Run { .. } + )); + + let mut saw_text = false; + let mut saw_finished = false; + let mut latest = 0; + for _ in 0..8 { + let frame = nagoya::timeout(Duration::from_secs(5), events.recv()) + .await + .expect("proxy should replay promptly") + .expect("replay frame should exist"); + if let ServerFrame::Event { + sequence, event, .. + } = frame + { + latest = sequence; + match event { + RunEvent::Text(text) if text == "survived restart" => saw_text = true, + RunEvent::Finished(_) => { + saw_finished = true; + break; + } + _ => {} } - _ => {} } } - } - assert!(saw_text, "replacement client receives the missed text"); - assert!( - saw_finished, - "replacement client receives the terminal outcome" - ); - assert!(latest > 0); - task.abort(); + assert!(saw_text, "replacement client receives the missed text"); + assert!( + saw_finished, + "replacement client receives the terminal outcome" + ); + assert!(latest > 0); + task.cancel(); + }); } diff --git a/crates/proxy/tests/websocket_mcp.rs b/crates/proxy/tests/websocket_mcp.rs index 280e19a..5a9a5a2 100644 --- a/crates/proxy/tests/websocket_mcp.rs +++ b/crates/proxy/tests/websocket_mcp.rs @@ -1,18 +1,19 @@ use agency_proxy::{RuntimeRegistry, WebSocketConfig, serve_websocket}; -use futures::{SinkExt, StreamExt}; +// endpoint-libs' own WebSocket client: nago-wss over a nagoya socket, the same +// stack the server speaks. It replaced tokio-tungstenite, which pulled tokio +// back into the test graph. +use endpoint_libs::libs::ws::{WsClient, WsClientBuilder}; +use nagoya::reactor::{Handle, Reactor}; use serde_json::{Value, json}; use std::{ net::TcpListener, process::{Command, Stdio}, - time::Duration, + time::{Duration, Instant}, }; use tempfile::tempdir; -use tokio_tungstenite::{ - connect_async, - tungstenite::{Message, client::IntoClientRequest, http::HeaderValue}, -}; const KEY: &str = "0123456789abcdef0123456789abcdef"; +const ORIGIN: &str = "https://agencyzero.example"; fn unused_loopback_address() -> std::net::SocketAddr { let listener = TcpListener::bind("127.0.0.1:0").expect("ephemeral listener should bind"); @@ -21,75 +22,93 @@ fn unused_loopback_address() -> std::net::SocketAddr { .expect("ephemeral listener should have an address") } -async fn receive_json(socket: &mut tokio_tungstenite::WebSocketStream) -> Value -where - S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin, -{ +// Every test starts its own reactor and keeps it for the whole test: the +// WebSocket client and the registry's provider processes are registered on it. +// `nagoya::block_on` polls no reactor, so a threaded one is what keeps those +// sockets moving while the test awaits. The server under test runs its own +// reactor on its own thread. + +/// Open a WebSocket to the proxy, offering the authentication subprotocol when +/// `authenticated`. Always sends the allowed `Origin`. +async fn connect( + address: std::net::SocketAddr, + authenticated: bool, + reactor: &Handle, +) -> eyre::Result { + let mut builder = WsClientBuilder::new().header("Origin", ORIGIN); + if authenticated { + builder = builder.protocol_header(format!("agency-proxy.{KEY}")); + } + let (client, _) = builder.build(&format!("ws://{address}"), reactor).await?; + Ok(client) +} + +/// Connect, retrying while the server is still binding. +async fn connect_when_ready( + address: std::net::SocketAddr, + authenticated: bool, + reactor: &Handle, +) -> WsClient { + loop { + match connect(address, authenticated, reactor).await { + Ok(client) => break client, + Err(_) => nagoya::sleep(Duration::from_millis(20)).await, + } + } +} + +async fn send_json(socket: &mut WsClient, value: Value) -> eyre::Result<()> { + socket.send_raw(value.to_string().as_bytes()).await +} + +async fn receive_json(socket: &mut WsClient) -> Value { loop { // Full workspace runs can contend with several process-backed transport // tests on CI. Keep the assertion bounded while allowing scheduler // headroom beyond the fake provider's intentional one-second delay. - let frame = tokio::time::timeout(Duration::from_secs(5), socket.next()) + let frame = nagoya::timeout(Duration::from_secs(5), socket.recv_raw()) .await - .expect("server should answer promptly") - .expect("server should keep the connection open") - .expect("server frame should be valid"); + .expect("server should answer promptly"); match frame { - Message::Text(text) => { - return serde_json::from_str(&text).expect("server should send JSON"); - } - Message::Binary(bytes) => { - return serde_json::from_slice(&bytes).expect("server should send JSON"); - } - Message::Ping(_) | Message::Pong(_) => {} - other => panic!("unexpected server frame: {other:?}"), + Ok(value) => return value, + // `recv_raw` has already answered a ping by the time it reports + // one, and it reports any non-text frame as an error. Skipping + // control frames is what the tungstenite loop did explicitly. + Err(error) + if error.to_string().ends_with("got ping") + || error.to_string().ends_with("got pong") => {} + Err(error) => panic!("server should send a JSON text frame: {error:#}"), } } } -#[tokio::test] -async fn authenticated_websocket_serves_the_minimal_mcp_surface() { - let address = unused_loopback_address(); - let task = tokio::spawn(serve_websocket( - RuntimeRegistry::default(), - WebSocketConfig { - address, - authentication_key: KEY.into(), - allowed_origins: vec!["https://agencyzero.example".into()], - tls: None, - }, - )); - - let mut request = format!("ws://{address}") - .into_client_request() - .expect("WebSocket request should build"); - request.headers_mut().insert( - "Sec-WebSocket-Protocol", - HeaderValue::from_str(&format!("agency-proxy.{KEY}")) - .expect("authentication protocol should be valid"), - ); - request.headers_mut().insert( - "Origin", - HeaderValue::from_static("https://agencyzero.example"), - ); +#[test] +fn authenticated_websocket_serves_the_minimal_mcp_surface() { + let reactor = Reactor::start().expect("test reactor should start"); + let handle = reactor.handle(); + nagoya::block_on(async { + let address = unused_loopback_address(); + let task = nagoya::spawn(serve_websocket( + RuntimeRegistry::new(handle.clone()), + WebSocketConfig { + address, + authentication_key: KEY.into(), + allowed_origins: vec![ORIGIN.into()], + }, + // Each test's server lives until the test process exits. None of them + // takes a signal, which is what lets several run in one process. + std::future::pending(), + )); - let (mut socket, _) = loop { - match connect_async(request.clone()).await { - Ok(connected) => break connected, - Err(_) => tokio::time::sleep(Duration::from_millis(20)).await, - } - }; - socket - .send(Message::Text( - json!({"method":1,"seq":1,"params":{}}).to_string().into(), - )) - .await - .expect("legacy frame should send"); - let rejected = receive_json(&mut socket).await; - assert_eq!(rejected["error"]["code"], -32600); + let mut socket = connect_when_ready(address, true, &handle).await; + send_json(&mut socket, json!({"method":1,"seq":1,"params":{}})) + .await + .expect("legacy frame should send"); + let rejected = receive_json(&mut socket).await; + assert_eq!(rejected["error"]["code"], -32600); - socket - .send(Message::Text( + send_json( + &mut socket, json!({ "jsonrpc":"2.0", "id":1, @@ -99,92 +118,78 @@ async fn authenticated_websocket_serves_the_minimal_mcp_surface() { "capabilities":{}, "clientInfo":{"name":"agencyzero-test","version":"0"} } - }) - .to_string() - .into(), - )) + }), + ) .await .expect("initialize should send"); - let initialized = receive_json(&mut socket).await; - assert_eq!(initialized["result"]["serverInfo"]["name"], "agency-proxy"); + let initialized = receive_json(&mut socket).await; + assert_eq!(initialized["result"]["serverInfo"]["name"], "agency-proxy"); - socket - .send(Message::Text( - json!({"jsonrpc":"2.0","method":"notifications/initialized"}) - .to_string() - .into(), - )) + send_json( + &mut socket, + json!({"jsonrpc":"2.0","method":"notifications/initialized"}), + ) .await .expect("initialized notification should send"); - socket - .send(Message::Text( - json!({"jsonrpc":"2.0","id":2,"method":"tools/list"}) - .to_string() - .into(), - )) + send_json( + &mut socket, + json!({"jsonrpc":"2.0","id":2,"method":"tools/list"}), + ) .await .expect("tools/list should send"); - let tools = receive_json(&mut socket).await; - let names = tools["result"]["tools"] - .as_array() - .expect("tools should be an array") - .iter() - .map(|tool| tool["name"].as_str().expect("tool should have a name")) - .collect::>(); - assert_eq!(names.len(), 11); - assert!(names.contains(&"list_runs")); - assert!(names.contains(&"start_run")); - assert!(names.contains(&"subscribe_run")); - assert!(names.contains(&"unsubscribe_run")); - assert!(!names.iter().any(|name| name.contains("shutdown"))); + let tools = receive_json(&mut socket).await; + let names = tools["result"]["tools"] + .as_array() + .expect("tools should be an array") + .iter() + .map(|tool| tool["name"].as_str().expect("tool should have a name")) + .collect::>(); + assert_eq!(names.len(), 11); + assert!(names.contains(&"list_runs")); + assert!(names.contains(&"start_run")); + assert!(names.contains(&"subscribe_run")); + assert!(names.contains(&"unsubscribe_run")); + assert!(!names.iter().any(|name| name.contains("shutdown"))); - socket - .send(Message::Text( + send_json( + &mut socket, json!({ "jsonrpc":"2.0", "id":3, "method":"tools/call", "params":{"name":"list_runs","arguments":{}} - }) - .to_string() - .into(), - )) + }), + ) .await .expect("list_runs should send"); - let runs = receive_json(&mut socket).await; - assert_eq!(runs["result"]["isError"], false); - assert_eq!(runs["result"]["structuredContent"]["runs"], json!([])); + let runs = receive_json(&mut socket).await; + assert_eq!(runs["result"]["isError"], false); + assert_eq!(runs["result"]["structuredContent"]["runs"], json!([])); - task.abort(); + task.cancel(); + }); } -#[tokio::test] -async fn websocket_rejects_clients_without_the_configured_key() { - let address = unused_loopback_address(); - let task = tokio::spawn(serve_websocket( - RuntimeRegistry::default(), - WebSocketConfig { - address, - authentication_key: KEY.into(), - allowed_origins: vec!["https://agencyzero.example".into()], - tls: None, - }, - )); - let mut request = format!("ws://{address}") - .into_client_request() - .expect("WebSocket request should build"); - request.headers_mut().insert( - "Origin", - HeaderValue::from_static("https://agencyzero.example"), - ); - let (mut socket, _) = loop { - match connect_async(request.clone()).await { - Ok(connected) => break connected, - Err(_) => tokio::time::sleep(Duration::from_millis(20)).await, - } - }; - socket - .send(Message::Text( +#[test] +fn websocket_rejects_clients_without_the_configured_key() { + let reactor = Reactor::start().expect("test reactor should start"); + let handle = reactor.handle(); + nagoya::block_on(async { + let address = unused_loopback_address(); + let task = nagoya::spawn(serve_websocket( + RuntimeRegistry::new(handle.clone()), + WebSocketConfig { + address, + authentication_key: KEY.into(), + allowed_origins: vec![ORIGIN.into()], + }, + // Each test's server lives until the test process exits. None of them + // takes a signal, which is what lets several run in one process. + std::future::pending(), + )); + let mut socket = connect_when_ready(address, false, &handle).await; + send_json( + &mut socket, json!({ "jsonrpc":"2.0", "id":1, @@ -194,78 +199,71 @@ async fn websocket_rejects_clients_without_the_configured_key() { "capabilities":{}, "clientInfo":{"name":"unauthenticated","version":"0"} } - }) - .to_string() - .into(), - )) + }), + ) .await .ok(); - let closed = tokio::time::timeout(Duration::from_secs(2), socket.next()) - .await - .expect("unauthenticated connection should close promptly"); - assert!( - !matches!(closed, Some(Ok(Message::Text(_)))), - "unauthenticated client must not receive an MCP response" - ); - task.abort(); + // `recv_raw` succeeds only for a JSON text frame, which is exactly the + // MCP response an unauthenticated client must not get. A close, a + // dropped connection or a control frame is an error here. + let closed = nagoya::timeout(Duration::from_secs(2), socket.recv_raw()) + .await + .expect("unauthenticated connection should close promptly"); + assert!( + closed.is_err(), + "unauthenticated client must not receive an MCP response" + ); + task.cancel(); + }); } -#[tokio::test] -async fn release_cli_starts_the_configured_websocket_transport() { - let address = unused_loopback_address(); - let dir = tempdir().expect("temporary config directory should exist"); - let config_path = dir.path().join("agency-proxy.json"); - std::fs::write( - &config_path, - json!({ - "connection": { - "type": "web_socket", - "address": address.to_string(), - "authenticationKey": KEY, - "allowedOrigins": ["https://agencyzero.example"] - } - }) - .to_string(), - ) - .expect("config should write"); - let mut child = Command::new(env!("CARGO_BIN_EXE_agency-proxy")) - .arg("--config") - .arg(&config_path) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .expect("AgencyProxy should launch"); +#[test] +fn release_cli_starts_the_configured_websocket_transport() { + let reactor = Reactor::start().expect("test reactor should start"); + let handle = reactor.handle(); + nagoya::block_on(async { + let address = unused_loopback_address(); + let dir = tempdir().expect("temporary config directory should exist"); + let config_path = dir.path().join("agency-proxy.json"); + std::fs::write( + &config_path, + json!({ + "connection": { + "type": "web_socket", + "address": address.to_string(), + "authenticationKey": KEY, + "allowedOrigins": [ORIGIN] + } + }) + .to_string(), + ) + .expect("config should write"); + let mut child = Command::new(env!("CARGO_BIN_EXE_agency-proxy")) + .arg("--config") + .arg(&config_path) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .expect("AgencyProxy should launch"); - let mut request = format!("ws://{address}") - .into_client_request() - .expect("WebSocket request should build"); - request.headers_mut().insert( - "Sec-WebSocket-Protocol", - HeaderValue::from_str(&format!("agency-proxy.{KEY}")) - .expect("authentication protocol should be valid"), - ); - request.headers_mut().insert( - "Origin", - HeaderValue::from_static("https://agencyzero.example"), - ); - let mut socket = None; - for _ in 0..100 { - if let Ok((connected, _)) = connect_async(request.clone()).await { - socket = Some(connected); - break; + let mut socket = None; + for _ in 0..100 { + if let Ok(connected) = connect(address, true, &handle).await { + socket = Some(connected); + break; + } + assert!( + child + .try_wait() + .expect("child status should be readable") + .is_none(), + "configured AgencyProxy exited before accepting WebSocket connections" + ); + nagoya::sleep(Duration::from_millis(20)).await; } - assert!( - child - .try_wait() - .expect("child status should be readable") - .is_none(), - "configured AgencyProxy exited before accepting WebSocket connections" - ); - tokio::time::sleep(Duration::from_millis(20)).await; - } - let mut socket = socket.expect("configured WebSocket should become reachable"); - socket - .send(Message::Text( + let mut socket = socket.expect("configured WebSocket should become reachable"); + send_json( + &mut socket, json!({ "jsonrpc":"2.0", "id":1, @@ -275,82 +273,72 @@ async fn release_cli_starts_the_configured_websocket_transport() { "capabilities":{}, "clientInfo":{"name":"agencyzero-cli-test","version":"0"} } - }) - .to_string() - .into(), - )) + }), + ) .await .expect("initialize should send"); - let initialized = receive_json(&mut socket).await; - assert_eq!(initialized["result"]["serverInfo"]["name"], "agency-proxy"); + let initialized = receive_json(&mut socket).await; + assert_eq!(initialized["result"]["serverInfo"]["name"], "agency-proxy"); - child.kill().expect("test proxy should stop"); - child.wait().expect("test proxy should be reaped"); + child.kill().expect("test proxy should stop"); + child.wait().expect("test proxy should be reaped"); + }); } #[cfg(unix)] -#[tokio::test] -async fn term_stops_websocket_admission_then_drains_owned_runs() { +#[test] +fn term_stops_websocket_admission_then_drains_owned_runs() { use std::os::unix::fs::PermissionsExt; - let address = unused_loopback_address(); - let dir = tempdir().expect("temporary config directory should exist"); - let provider = dir.path().join("slow-claude"); - std::fs::write( - &provider, - r#"#!/bin/sh + let reactor = Reactor::start().expect("test reactor should start"); + let handle = reactor.handle(); + nagoya::block_on(async { + let address = unused_loopback_address(); + let dir = tempdir().expect("temporary config directory should exist"); + let provider = dir.path().join("slow-claude"); + std::fs::write( + &provider, + r#"#!/bin/sh sleep 2 printf '%s\n' '{"type":"result","subtype":"success","is_error":false,"result":"done","session_id":"proxy-session","usage":{"input_tokens":1,"output_tokens":1}}' "#, - ) - .expect("fake provider should write"); - std::fs::set_permissions(&provider, std::fs::Permissions::from_mode(0o700)) - .expect("fake provider should be executable"); - let config_path = dir.path().join("agency-proxy.json"); - std::fs::write( - &config_path, - json!({ - "connection": { - "type": "web_socket", - "address": address.to_string(), - "authenticationKey": KEY, - "allowedOrigins": ["https://agencyzero.example"] - } - }) - .to_string(), - ) - .expect("config should write"); - let mut child = Command::new(env!("CARGO_BIN_EXE_agency-proxy")) - .arg("--config") - .arg(&config_path) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .expect("AgencyProxy should launch"); + ) + .expect("fake provider should write"); + std::fs::set_permissions(&provider, std::fs::Permissions::from_mode(0o700)) + .expect("fake provider should be executable"); + let config_path = dir.path().join("agency-proxy.json"); + std::fs::write( + &config_path, + json!({ + "connection": { + "type": "web_socket", + "address": address.to_string(), + "authenticationKey": KEY, + "allowedOrigins": [ORIGIN] + } + }) + .to_string(), + ) + .expect("config should write"); + let mut child = Command::new(env!("CARGO_BIN_EXE_agency-proxy")) + .arg("--config") + .arg(&config_path) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .expect("AgencyProxy should launch"); - let mut request = format!("ws://{address}") - .into_client_request() - .expect("WebSocket request should build"); - request.headers_mut().insert( - "Sec-WebSocket-Protocol", - HeaderValue::from_str(&format!("agency-proxy.{KEY}")) - .expect("authentication protocol should be valid"), - ); - request.headers_mut().insert( - "Origin", - HeaderValue::from_static("https://agencyzero.example"), - ); - let mut socket = None; - for _ in 0..100 { - if let Ok((connected, _)) = connect_async(request.clone()).await { - socket = Some(connected); - break; + let mut socket = None; + for _ in 0..100 { + if let Ok(connected) = connect(address, true, &handle).await { + socket = Some(connected); + break; + } + nagoya::sleep(Duration::from_millis(20)).await; } - tokio::time::sleep(Duration::from_millis(20)).await; - } - let mut socket = socket.expect("configured WebSocket should become reachable"); - socket - .send(Message::Text( + let mut socket = socket.expect("configured WebSocket should become reachable"); + send_json( + &mut socket, json!({ "jsonrpc":"2.0", "id":1, @@ -360,23 +348,19 @@ printf '%s\n' '{"type":"result","subtype":"success","is_error":false,"result":"d "capabilities":{}, "clientInfo":{"name":"agencyzero-drain-test","version":"0"} } - }) - .to_string() - .into(), - )) + }), + ) .await .expect("initialize should send"); - let _ = receive_json(&mut socket).await; - socket - .send(Message::Text( - json!({"jsonrpc":"2.0","method":"notifications/initialized"}) - .to_string() - .into(), - )) + let _ = receive_json(&mut socket).await; + send_json( + &mut socket, + json!({"jsonrpc":"2.0","method":"notifications/initialized"}), + ) .await .expect("initialized notification should send"); - socket - .send(Message::Text( + send_json( + &mut socket, json!({ "jsonrpc":"2.0", "id":2, @@ -405,116 +389,99 @@ printf '%s\n' '{"type":"result","subtype":"success","is_error":false,"result":"d } } } - }) - .to_string() - .into(), - )) + }), + ) .await .expect("start_run should send"); - let started = receive_json(&mut socket).await; - assert_eq!(started["result"]["isError"], false); + let started = receive_json(&mut socket).await; + assert_eq!(started["result"]["isError"], false); - // SAFETY: `child.id()` is the dedicated AgencyProxy process created by - // this test, and SIGTERM is the behavior under test. - let sent = unsafe { libc::kill(child.id() as i32, libc::SIGTERM) }; - assert_eq!(sent, 0, "SIGTERM should reach the test proxy"); - tokio::time::sleep(Duration::from_millis(100)).await; - assert!( - child - .try_wait() - .expect("child status should be readable") - .is_none(), - "proxy must remain alive while the accepted provider run drains" - ); - - let deadline = tokio::time::Instant::now() + Duration::from_secs(5); - loop { - if child - .try_wait() - .expect("child status should be readable") - .is_some() - { - break; - } + // SAFETY: `child.id()` is the dedicated AgencyProxy process created by + // this test, and SIGTERM is the behavior under test. + let sent = unsafe { libc::kill(child.id() as i32, libc::SIGTERM) }; + assert_eq!(sent, 0, "SIGTERM should reach the test proxy"); + nagoya::sleep(Duration::from_millis(100)).await; assert!( - tokio::time::Instant::now() < deadline, - "proxy should exit after the provider run settles" + child + .try_wait() + .expect("child status should be readable") + .is_none(), + "proxy must remain alive while the accepted provider run drains" ); - tokio::time::sleep(Duration::from_millis(25)).await; - } + + let deadline = Instant::now() + Duration::from_secs(5); + loop { + if child + .try_wait() + .expect("child status should be readable") + .is_some() + { + break; + } + assert!( + Instant::now() < deadline, + "proxy should exit after the provider run settles" + ); + nagoya::sleep(Duration::from_millis(25)).await; + } + }); } #[cfg(unix)] -#[tokio::test] -async fn subscribe_replays_then_pushes_sequence_numbered_mcp_notifications() { +#[test] +fn subscribe_replays_then_pushes_sequence_numbered_mcp_notifications() { use std::os::unix::fs::PermissionsExt; - let address = unused_loopback_address(); - let dir = tempdir().expect("temporary provider directory should exist"); - let provider = dir.path().join("eventful-claude"); - std::fs::write( - &provider, - r#"#!/bin/sh + let reactor = Reactor::start().expect("test reactor should start"); + let handle = reactor.handle(); + nagoya::block_on(async { + let address = unused_loopback_address(); + let dir = tempdir().expect("temporary provider directory should exist"); + let provider = dir.path().join("eventful-claude"); + std::fs::write( + &provider, + r#"#!/bin/sh sleep 1 printf '%s\n' '{"type":"result","subtype":"success","is_error":false,"result":"done","session_id":"proxy-session","usage":{"input_tokens":1,"output_tokens":1}}' "#, - ) - .expect("fake provider should write"); - std::fs::set_permissions(&provider, std::fs::Permissions::from_mode(0o700)) - .expect("fake provider should be executable"); + ) + .expect("fake provider should write"); + std::fs::set_permissions(&provider, std::fs::Permissions::from_mode(0o700)) + .expect("fake provider should be executable"); - let task = tokio::spawn(serve_websocket( - RuntimeRegistry::default(), - WebSocketConfig { - address, - authentication_key: KEY.into(), - allowed_origins: vec!["https://agencyzero.example".into()], - tls: None, - }, - )); - let mut request = format!("ws://{address}") - .into_client_request() - .expect("WebSocket request should build"); - request.headers_mut().insert( - "Sec-WebSocket-Protocol", - HeaderValue::from_str(&format!("agency-proxy.{KEY}")) - .expect("authentication protocol should be valid"), - ); - request.headers_mut().insert( - "Origin", - HeaderValue::from_static("https://agencyzero.example"), - ); - let (mut socket, _) = loop { - match connect_async(request.clone()).await { - Ok(connected) => break connected, - Err(_) => tokio::time::sleep(Duration::from_millis(20)).await, - } - }; - socket - .send(Message::Text( + let task = nagoya::spawn(serve_websocket( + RuntimeRegistry::new(handle.clone()), + WebSocketConfig { + address, + authentication_key: KEY.into(), + allowed_origins: vec![ORIGIN.into()], + }, + // Each test's server lives until the test process exits. None of them + // takes a signal, which is what lets several run in one process. + std::future::pending(), + )); + let mut socket = connect_when_ready(address, true, &handle).await; + send_json( + &mut socket, json!({ "jsonrpc":"2.0","id":1,"method":"initialize", "params":{ "protocolVersion":"2025-06-18","capabilities":{}, "clientInfo":{"name":"subscription-test","version":"0"} } - }) - .to_string() - .into(), - )) + }), + ) .await .expect("initialize should send"); - let _ = receive_json(&mut socket).await; - socket - .send(Message::Text( - json!({"jsonrpc":"2.0","method":"notifications/initialized"}) - .to_string() - .into(), - )) + let _ = receive_json(&mut socket).await; + send_json( + &mut socket, + json!({"jsonrpc":"2.0","method":"notifications/initialized"}), + ) .await .expect("initialized notification should send"); - socket - .send(Message::Text( + send_json( + &mut socket, json!({ "jsonrpc":"2.0","id":2,"method":"tools/call", "params":{ @@ -532,52 +499,49 @@ printf '%s\n' '{"type":"result","subtype":"success","is_error":false,"result":"d } } } - }) - .to_string() - .into(), - )) + }), + ) .await .expect("start_run should send"); - let started = receive_json(&mut socket).await; - assert_eq!(started["result"]["isError"], false); + let started = receive_json(&mut socket).await; + assert_eq!(started["result"]["isError"], false); - socket - .send(Message::Text( + send_json( + &mut socket, json!({ "jsonrpc":"2.0","id":3,"method":"tools/call", "params":{ "name":"subscribe_run", "arguments":{"runId":"subscribed","afterSequence":0} } - }) - .to_string() - .into(), - )) + }), + ) .await .expect("subscribe_run should send"); - let subscribed = receive_json(&mut socket).await; - assert_eq!(subscribed["id"], 3); - assert_eq!(subscribed["result"]["isError"], false); - let replayed_through = subscribed["result"]["structuredContent"]["events"] - .as_array() - .expect("subscribe result should contain replay") - .last() - .and_then(|event| event["sequence"].as_u64()) - .unwrap_or(0); + let subscribed = receive_json(&mut socket).await; + assert_eq!(subscribed["id"], 3); + assert_eq!(subscribed["result"]["isError"], false); + let replayed_through = subscribed["result"]["structuredContent"]["events"] + .as_array() + .expect("subscribe result should contain replay") + .last() + .and_then(|event| event["sequence"].as_u64()) + .unwrap_or(0); - let notification = loop { - let frame = receive_json(&mut socket).await; - if frame["method"] == "notifications/run_event" { - break frame; - } - }; - assert_eq!(notification["params"]["runId"], "subscribed"); - assert!( - notification["params"]["sequence"] - .as_u64() - .expect("notification should carry a sequence") - > replayed_through - ); + let notification = loop { + let frame = receive_json(&mut socket).await; + if frame["method"] == "notifications/run_event" { + break frame; + } + }; + assert_eq!(notification["params"]["runId"], "subscribed"); + assert!( + notification["params"]["sequence"] + .as_u64() + .expect("notification should carry a sequence") + > replayed_through + ); - task.abort(); + task.cancel(); + }); }