From ba612675e3aeec49bc4db066ce92925016cbd449 Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 23 Sep 2026 06:37:53 +0700 Subject: [PATCH 1/5] Take endpoint-libs 3.2 3.2 removed the tokio flavour of the transport. `framed_json_with_max_frame` is gone and `framed_json_neutral_with_max_frame` replaces it, taking a `futures_io` stream, so the tokio `UnixStream`s here bridge through tokio-util's `compat`. The frame cap and the codec on the wire are unchanged. `listen` also stopped being a future. It builds its own reactor, registers SIGTERM and SIGINT on that reactor, and blocks until one fires, so the `.await` comes off and the drain that follows it runs exactly as before. The requirement was already `^3` and needed no edit: the lock was pinning 3.0.0. --- Cargo.lock | 1364 ++++------------------------ crates/client/Cargo.toml | 1 + crates/client/src/lib.rs | 8 +- crates/proxy/Cargo.toml | 1 + crates/proxy/src/lib.rs | 8 +- crates/proxy/src/web.rs | 5 +- crates/proxy/tests/local_socket.rs | 8 +- 7 files changed, 217 insertions(+), 1178 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1274e91..e71c192 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,6 +21,7 @@ dependencies = [ "thiserror", "tokio", "tokio-tungstenite", + "tokio-util", ] [[package]] @@ -33,6 +34,7 @@ dependencies = [ "serde_json", "thiserror", "tokio", + "tokio-util", ] [[package]] @@ -164,12 +166,6 @@ dependencies = [ "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" @@ -439,6 +435,12 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.92" @@ -451,10 +453,14 @@ dependencies = [ ] [[package]] -name = "atomic-waker" -version = "1.1.2" +name = "atomic-wait" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +checksum = "a55b94919229f2c42292fd71ffa4b75e83193bffdd77b1e858cd55fd2d0b0ea8" +dependencies = [ + "libc", + "windows-sys 0.42.0", +] [[package]] name = "auto_impl" @@ -473,49 +479,6 @@ 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" @@ -655,23 +618,6 @@ 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" @@ -787,16 +733,6 @@ 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" @@ -814,9 +750,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" dependencies = [ "libc", ] @@ -831,25 +767,20 @@ dependencies = [ ] [[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" +name = "crossbeam-queue" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111ce8f7abfbac38b4bc4f32a3a2dda1a8034b873c90c7899f302cc9dbbc05ec" +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" dependencies = [ "crossbeam-utils", - "futures-core", - "parking_lot", - "pointers", - "smallvec", - "tokio", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + [[package]] name = "crunchy" version = "0.2.4" @@ -991,17 +922,6 @@ dependencies = [ "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" @@ -1059,20 +979,11 @@ dependencies = [ "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" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64708d7972a5c06112029ef04599061238abc55ba5e879a0e02dfc373d24df34" +checksum = "a8fe88a4b4499267a0f3caacda11cef978adf1fe8b37f7f2ee6ae3fab237a5a6" dependencies = [ "alloy-primitives", "async-trait", @@ -1080,41 +991,22 @@ dependencies = [ "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", + "nago-wss", + "nagoya", + "parking_lot_lite_hack", "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]] @@ -1230,10 +1122,13 @@ dependencies = [ ] [[package]] -name = "fnv" -version = "1.0.7" +name = "flexpect" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +checksum = "91463e4ffff882636c3bf769011bd013857c57c9485cd0e8f3be0cace23ef7db" +dependencies = [ + "rustversion", +] [[package]] name = "foldhash" @@ -1241,15 +1136,6 @@ 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" @@ -1372,10 +1258,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi", - "wasm-bindgen", ] [[package]] @@ -1397,11 +1281,8 @@ 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]] @@ -1415,25 +1296,6 @@ dependencies = [ "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" @@ -1501,41 +1363,12 @@ dependencies = [ "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" @@ -1545,82 +1378,6 @@ 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" @@ -1645,109 +1402,6 @@ 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" @@ -1797,12 +1451,6 @@ dependencies = [ "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" @@ -1836,15 +1484,6 @@ 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" @@ -1882,7 +1521,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffd9697dc4a9a62e2da93389f34400b77a28f0287711263cabb203b3ccb9c0e4" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", ] [[package]] @@ -1934,12 +1573,6 @@ 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" @@ -1955,12 +1588,6 @@ 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" @@ -1970,12 +1597,6 @@ 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" @@ -1983,14 +1604,8 @@ 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" +name = "mio" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ @@ -1999,6 +1614,33 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nago-wss" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff98af5c923e68ee668fceb0c8fbe0ae6051b72fb8674949855455f558c18bd" +dependencies = [ + "bytes", + "libc", + "nagoya", + "ps-simdutf8", +] + +[[package]] +name = "nagoya" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9081694cf4bb0358e2f3a7730022cb916da76a64a248b074464ca2d8eddb4ba3" +dependencies = [ + "async-task", + "bytes", + "futures-io", + "futures-util", + "libc", + "ps-st3", + "spin", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -2055,98 +1697,6 @@ 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" @@ -2176,26 +1726,39 @@ dependencies = [ ] [[package]] -name = "parking_lot" -version = "0.12.5" +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 = "parking_lot_lite_hack" +version = "0.12.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +checksum = "4335701c28bc0ae2ca096e9693277a1e638c767472c41e41a3d22d0affd49508" dependencies = [ "lock_api", - "parking_lot_core", + "parking_lot_lite_hack_core", ] [[package]] -name = "parking_lot_core" -version = "0.9.12" +name = "parking_lot_lite_hack_core" +version = "0.9.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +checksum = "b67f4481ce4fb7c3e3e4b602b0aca8f207e4dd89e3b7873ea2b3f57f158b5dc0" dependencies = [ "cfg-if", "libc", - "redox_syscall", "smallvec", "windows-link", + "windows-sys 0.60.2", ] [[package]] @@ -2204,12 +1767,6 @@ 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" @@ -2220,26 +1777,6 @@ dependencies = [ "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" @@ -2256,21 +1793,6 @@ dependencies = [ "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" @@ -2331,82 +1853,26 @@ dependencies = [ ] [[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" +name = "ps-simdutf8" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +checksum = "4728371618f944f51f8ad94cd42963dd9a2b78a23689a1819b47ac0e3b060dda" 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", + "cpufeatures 0.3.1", + "flexpect", + "rustversion", ] [[package]] -name = "quinn-udp" -version = "0.5.15" +name = "ps-st3" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +checksum = "a78c6922d75f35bd19b6904f10f394c59104f70d1e87f4519d3e62fc04f02ac6" dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.61.2", + "atomic-wait", + "crossbeam-queue", + "crossbeam-utils", + "spin", ] [[package]] @@ -2457,17 +1923,6 @@ dependencies = [ "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" @@ -2506,21 +1961,6 @@ 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" @@ -2585,49 +2025,6 @@ 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" @@ -2638,20 +2035,6 @@ dependencies = [ "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" @@ -2740,54 +2123,12 @@ dependencies = [ "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" @@ -2930,18 +2271,6 @@ dependencies = [ "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" @@ -3060,6 +2389,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" + [[package]] name = "spki" version = "0.7.3" @@ -3070,12 +2405,6 @@ dependencies = [ "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" @@ -3133,47 +2462,6 @@ dependencies = [ "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" @@ -3252,16 +2540,6 @@ dependencies = [ "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" @@ -3286,7 +2564,6 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", @@ -3305,27 +2582,6 @@ dependencies = [ "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" @@ -3334,12 +2590,8 @@ checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" dependencies = [ "futures-util", "log", - "rustls", - "rustls-pki-types", "tokio", - "tokio-rustls", "tungstenite", - "webpki-roots 0.26.11", ] [[package]] @@ -3350,8 +2602,8 @@ checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", - "libc", "pin-project-lite", "tokio", ] @@ -3386,95 +2638,6 @@ 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" @@ -3531,22 +2694,6 @@ dependencies = [ "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" @@ -3565,12 +2712,6 @@ dependencies = [ "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" @@ -3583,8 +2724,6 @@ dependencies = [ "httparse", "log", "rand 0.9.5", - "rustls", - "rustls-pki-types", "sha1", "thiserror", ] @@ -3637,36 +2776,12 @@ 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" @@ -3697,15 +2812,6 @@ 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" @@ -3734,16 +2840,6 @@ dependencies = [ "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" @@ -3776,44 +2872,6 @@ 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" @@ -3877,17 +2935,6 @@ 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" @@ -3908,9 +2955,24 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.52.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ "windows-targets", ] @@ -3926,67 +2988,110 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.6" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.6" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" -version = "0.52.6" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" -version = "0.52.6" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" -version = "0.52.6" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" -version = "0.52.6" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" @@ -4003,12 +3108,6 @@ 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" @@ -4018,29 +3117,6 @@ 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" @@ -4061,27 +3137,6 @@ dependencies = [ "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" @@ -4102,39 +3157,6 @@ dependencies = [ "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" diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index b2d6b57..e5ae2d1 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true repository.workspace = true [dependencies] +tokio-util = { version = "^0.7", features = ["compat"] } agency-proxy-protocol.workspace = true endpoint-libs.workspace = true futures.workspace = true diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index 41d17b0..6c01295 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -2,7 +2,10 @@ 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}; +// endpoint-libs 3.2 removed the tokio flavour of the transport; the neutral +// framing takes a `futures_io` stream, so a tokio `UnixStream` bridges through +// tokio-util's `compat`. +use endpoint_libs::libs::ws::{WireMessage, transport::framed::framed_json_neutral_with_max_frame}; use futures::{SinkExt, StreamExt}; use std::{collections::BTreeMap, path::Path}; use thiserror::Error; @@ -10,6 +13,7 @@ use tokio::{ net::UnixStream, sync::{broadcast, mpsc, oneshot}, }; +use tokio_util::compat::TokioAsyncReadCompatExt; #[derive(Debug, Error)] pub enum ClientError { @@ -38,7 +42,7 @@ pub struct Client { 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 transport = framed_json_neutral_with_max_frame(stream.compat(), MAX_FRAME_BYTES + 1); let (requests, request_rx) = mpsc::channel(64); let (events, _) = broadcast::channel(512); tokio::spawn(drive(transport, request_rx, events.clone())); diff --git a/crates/proxy/Cargo.toml b/crates/proxy/Cargo.toml index bccb9b6..dcba5a5 100644 --- a/crates/proxy/Cargo.toml +++ b/crates/proxy/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true repository.workspace = true [dependencies] +tokio-util = { version = "^0.7", features = ["compat"] } agent-abstraction = "0.4.21" agency-proxy-protocol.workspace = true async-trait.workspace = true diff --git a/crates/proxy/src/lib.rs b/crates/proxy/src/lib.rs index 9c14d97..4169997 100644 --- a/crates/proxy/src/lib.rs +++ b/crates/proxy/src/lib.rs @@ -8,7 +8,10 @@ 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}; +// endpoint-libs 3.2 removed the tokio flavour of the transport; the neutral +// framing takes a `futures_io` stream, so a tokio `UnixStream` bridges through +// tokio-util's `compat`. +use endpoint_libs::libs::ws::{WireMessage, transport::framed::framed_json_neutral_with_max_frame}; use futures::{Sink, SinkExt, Stream, StreamExt}; use std::{ collections::BTreeMap, @@ -17,6 +20,7 @@ use std::{ }; use thiserror::Error; use tokio::net::{UnixListener, UnixStream}; +use tokio_util::compat::TokioAsyncReadCompatExt; pub use config::{ConfigError, ConnectionConfig, ProxyConfig, TlsConfig}; pub use runtime::{Attachment, RuntimeError, RuntimeRegistry, SequencedEvent}; @@ -144,7 +148,7 @@ async fn handle_connection( request_shutdown: tokio::sync::watch::Sender, 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(stream.compat(), MAX_FRAME_BYTES + 1); let Some(first) = receive_client(&mut transport).await? else { return Ok(()); }; diff --git a/crates/proxy/src/web.rs b/crates/proxy/src/web.rs index daa8d21..bfa46e6 100644 --- a/crates/proxy/src/web.rs +++ b/crates/proxy/src/web.rs @@ -117,7 +117,10 @@ pub async fn serve_websocket( version: env!("CARGO_PKG_VERSION").into(), }, )?; - let result = server.listen().await; + // `listen` blocks until a signal arrives in endpoint-libs 3: it builds its + // own reactor, registers SIGTERM and SIGINT on it, and returns when either + // fires. + let result = server.listen(); let active = drain_registry.active_count().await; if active > 0 { eprintln!("AgencyProxy WebSocket admission closed; draining {active} active run(s)"); diff --git a/crates/proxy/tests/local_socket.rs b/crates/proxy/tests/local_socket.rs index 374d3c7..01e2ec4 100644 --- a/crates/proxy/tests/local_socket.rs +++ b/crates/proxy/tests/local_socket.rs @@ -4,11 +4,15 @@ 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}; +// endpoint-libs 3.2 removed the tokio flavour of the transport; the neutral +// framing takes a `futures_io` stream, so a tokio `UnixStream` bridges through +// tokio-util's `compat`. +use endpoint_libs::libs::ws::{WireMessage, transport::framed::framed_json_neutral_with_max_frame}; use futures::{SinkExt, StreamExt}; use std::{collections::BTreeMap, os::unix::fs::PermissionsExt, time::Duration}; use tempfile::tempdir; use tokio::net::UnixStream; +use tokio_util::compat::TokioAsyncReadCompatExt; #[tokio::test] async fn hello_then_list_runs_uses_the_versioned_local_protocol() { @@ -403,7 +407,7 @@ async fn commands_before_hello_are_rejected() { let stream = UnixStream::connect(&socket) .await .expect("client should connect"); - let mut transport = framed_json_with_max_frame(stream, MAX_FRAME_BYTES + 1); + let mut transport = framed_json_neutral_with_max_frame(stream.compat(), MAX_FRAME_BYTES + 1); let frame = ClientFrame { request_id: 9, message: ClientMessage::ListRuns, From 966b194e249eb6bdf58067f036284f8699c2fc84 Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 23 Sep 2026 11:54:50 +0700 Subject: [PATCH 2/5] Serve WebSocket the way endpoint-libs 3 serves Three things broke when endpoint-libs moved its server onto nagoya, and the WebSocket suite hung on them. listen() now blocks its thread for the life of the server. serve_websocket called it from an async fn, which held a tokio worker forever and, on the current-thread runtime every test uses, the only one. The server now runs on a thread of its own and serve_websocket awaits its answer. listen() also claimed SIGTERM and SIGINT for the process, and nagoya allows one waiter per signal, so the second server in a test process failed with EBUSY after binding and its client waited on a dead port. Each test passed alone and hung together. serve_websocket now takes a stop future and uses listen_until (endpoint-libs 3.3); main owns the signals through tokio and forwards the first one over a oneshot, keeping the SIGTERM drain contract. A handler is polled in place on the connection task with no spawner, so the run-event forwarder could not spawn_local. It runs on the registry's tokio executor, where provider work already runs; the toolbox is Send + Sync. TLS goes: endpoint-libs 3 refuses certificates outright and terminates TLS at the edge, and this listener is loopback-only by validation anyway. Outbound HTTPS is untouched; this crate has no TLS client, the provider CLIs make those calls themselves. --- Cargo.toml | 4 +- crates/proxy/src/config.rs | 12 ---- crates/proxy/src/lib.rs | 4 +- crates/proxy/src/main.rs | 65 ++++++++++++++------ crates/proxy/src/runtime.rs | 13 ++++ crates/proxy/src/web.rs | 94 ++++++++++++++++++----------- crates/proxy/tests/websocket_mcp.rs | 12 +++- 7 files changed, 131 insertions(+), 73 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 829bb8c..85333d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,13 +15,13 @@ agency-proxy-protocol = { version = "^0.1.9", path = "crates/protocol" } agency-proxy-client = { version = "^0.1.9", path = "crates/client" } async-trait = "0.1" clap = { version = "4", features = ["derive"] } -endpoint-libs = { version = "^3", default-features = false, features = ["framed-transport"] } +endpoint-libs = { version = "^3.3", default-features = false, features = ["framed-transport"] } eyre = "0.6" futures = "0.3" serde = { version = "1", features = ["derive"] } serde_json = "1" thiserror = "2" -tokio = { version = "1", features = ["fs", "io-util", "macros", "net", "rt-multi-thread", "sync", "time"] } +tokio = { version = "1", features = ["fs", "io-util", "macros", "net", "rt-multi-thread", "signal", "sync", "time"] } [profile.release] strip = true diff --git a/crates/proxy/src/config.rs b/crates/proxy/src/config.rs index 24110ef..b2f92eb 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}")] @@ -66,7 +57,6 @@ impl ProxyConfig { address, authentication_key, allowed_origins, - tls: _, } = &self.connection else { return Ok(()); @@ -119,7 +109,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 +121,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/lib.rs b/crates/proxy/src/lib.rs index 4169997..5dfdf51 100644 --- a/crates/proxy/src/lib.rs +++ b/crates/proxy/src/lib.rs @@ -22,9 +22,9 @@ use thiserror::Error; use tokio::net::{UnixListener, UnixStream}; use tokio_util::compat::TokioAsyncReadCompatExt; -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 { diff --git a/crates/proxy/src/main.rs b/crates/proxy/src/main.rs index 4f592c5..7456c6f 100644 --- a/crates/proxy/src/main.rs +++ b/crates/proxy/src/main.rs @@ -1,6 +1,5 @@ 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}; @@ -42,24 +41,34 @@ async fn main() -> ExitCode { address, authentication_key, allowed_origins, - tls, - } => { - eprintln!("AgencyProxy MCP/WebSocket listening on {address}"); - serve_websocket( - registry, - WebSocketConfig { - address, - authentication_key, - allowed_origins, - tls: tls.map(|tls| WebSocketTlsConfig { - certificates: tls.certificates, - private_key: tls.private_key, - }), - }, - ) - .await - .map_err(|error| error.to_string()) - } + } => match termination() { + Err(error) => Err(format!("could not register SIGTERM and SIGINT: {error}")), + Ok(terminated) => { + 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, where tokio's signal + // driver does not reach, so the signal is waited for here and + // forwarded through a oneshot, which needs no runtime to await. + let (stop, stopped) = tokio::sync::oneshot::channel::<()>(); + tokio::spawn(async move { + terminated.await; + let _ = stop.send(()); + }); + serve_websocket( + registry, + WebSocketConfig { + address, + authentication_key, + allowed_origins, + }, + async move { + let _ = stopped.await; + }, + ) + .await + .map_err(|error| error.to_string()) + } + }, }; match result { Ok(()) => ExitCode::SUCCESS, @@ -70,6 +79,22 @@ async fn main() -> ExitCode { } } +/// 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. +fn termination() -> std::io::Result> { + use tokio::signal::unix::{SignalKind, signal}; + let mut terminate = signal(SignalKind::terminate())?; + let mut interrupt = signal(SignalKind::interrupt())?; + Ok(async move { + tokio::select! { + _ = terminate.recv() => {} + _ = interrupt.recv() => {} + } + }) +} + async fn serve_unix(socket: PathBuf, registry: RuntimeRegistry) -> Result<(), String> { let server = ProxyServer::bind_with_registry(&socket, registry) .await diff --git a/crates/proxy/src/runtime.rs b/crates/proxy/src/runtime.rs index c4ad994..e304e3f 100644 --- a/crates/proxy/src/runtime.rs +++ b/crates/proxy/src/runtime.rs @@ -113,6 +113,19 @@ pub enum RuntimeError { } impl RuntimeRegistry { + /// Run `future` on the runtime that owns this registry's provider tasks. + /// + /// For work started from a transport whose own executor is not tokio's, + /// such as an endpoint-libs WebSocket handler, which is polled on that + /// server's reactor and can neither `spawn_local` nor reach tokio's timers. + pub(crate) fn spawn(&self, future: F) -> tokio::task::JoinHandle + where + F: std::future::Future + Send + 'static, + F::Output: Send + 'static, + { + self.executor.spawn(future) + } + pub async fn account_usage(&self) -> Vec { futures::future::join_all( [Agent::Claude, Agent::Codex, Agent::Copilot, Agent::Grok].map(|agent| async move { diff --git a/crates/proxy/src/web.rs b/crates/proxy/src/web.rs index bfa46e6..23171f4 100644 --- a/crates/proxy/src/web.rs +++ b/crates/proxy/src/web.rs @@ -22,7 +22,6 @@ use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, net::SocketAddr, - path::PathBuf, sync::{ Arc, Mutex, atomic::{AtomicU64, Ordering}, @@ -36,13 +35,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 +63,66 @@ 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, so it must not need tokio: a 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 tokio worker that held the worker for the life of the + // process, and on a current-thread runtime, which every test uses, it held + // the only one. It gets a thread of its own, and this future waits for its + // answer without blocking. + let (answer, answered) = tokio::sync::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,22 +154,7 @@ pub async fn serve_websocket( version: env!("CARGO_PKG_VERSION").into(), }, )?; - // `listen` blocks until a signal arrives in endpoint-libs 3: it builds its - // own reactor, registers SIGTERM and SIGINT on it, and returns when either - // fires. - let result = server.listen(); - 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); @@ -392,7 +414,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 runtime, not the connection's. endpoint-libs 3 polls + // a handler in place on the connection task and has no spawner, and + // `spawn_local` needs a tokio LocalSet this thread no longer has. The + // forwarder needs nothing thread-bound: the toolbox is `Send + Sync`. + let task = self.registry.spawn(async move { forward_run_events( registry, toolbox, diff --git a/crates/proxy/tests/websocket_mcp.rs b/crates/proxy/tests/websocket_mcp.rs index 280e19a..aff6d4a 100644 --- a/crates/proxy/tests/websocket_mcp.rs +++ b/crates/proxy/tests/websocket_mcp.rs @@ -56,8 +56,10 @@ async fn authenticated_websocket_serves_the_minimal_mcp_surface() { address, authentication_key: KEY.into(), allowed_origins: vec!["https://agencyzero.example".into()], - tls: None, }, + // 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 request = format!("ws://{address}") @@ -167,8 +169,10 @@ async fn websocket_rejects_clients_without_the_configured_key() { address, authentication_key: KEY.into(), allowed_origins: vec!["https://agencyzero.example".into()], - tls: None, }, + // 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 request = format!("ws://{address}") .into_client_request() @@ -469,8 +473,10 @@ printf '%s\n' '{"type":"result","subtype":"success","is_error":false,"result":"d address, authentication_key: KEY.into(), allowed_origins: vec!["https://agencyzero.example".into()], - tls: None, }, + // 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 request = format!("ws://{address}") .into_client_request() From 0ed2dd61e6aa02ac8fea17ebb2eb1317e0fa48fc Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 23 Sep 2026 11:54:51 +0700 Subject: [PATCH 3/5] Release 0.1.11 Builds against endpoint-libs 3.3; 0.1.10 no longer compiles against 3.2. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 85333d7..d1bfdc2 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 From 67fc25b7bf421e37e3e6c2762f7da9edcae4af01 Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 23 Sep 2026 18:41:08 +0700 Subject: [PATCH 4/5] Stop tracking Cargo.lock, and take our own crates by broad carets No lockfile: CI and consumers resolve the manifest ranges afresh, so a release of one of our crates reaches this one without a commit here. CI drops --locked, which only meant "fail when the ranges moved", and publish no longer triggers on a file that is gone. Our own crates are required at the major line (^0.1, ^0.4, ^3) rather than a patch floor: with no lockfile the newest release is what resolves, which for endpoint-libs is 3.3 and its listen_until. --- .github/workflows/ci.yml | 2 +- .github/workflows/publish.yml | 1 - .gitignore | 1 + Cargo.lock | 3164 --------------------------------- Cargo.toml | 6 +- crates/proxy/Cargo.toml | 2 +- 6 files changed, 6 insertions(+), 3170 deletions(-) delete mode 100644 Cargo.lock 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 e71c192..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,3164 +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", - "tokio-util", -] - -[[package]] -name = "agency-proxy-client" -version = "0.1.10" -dependencies = [ - "agency-proxy-protocol", - "endpoint-libs", - "futures", - "serde_json", - "thiserror", - "tokio", - "tokio-util", -] - -[[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 = "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-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[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-wait" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55b94919229f2c42292fd71ffa4b75e83193bffdd77b1e858cd55fd2d0b0ea8" -dependencies = [ - "libc", - "windows-sys 0.42.0", -] - -[[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 = "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 = "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-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.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" -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-queue" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" -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 = "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 = "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 = "endpoint-libs" -version = "3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8fe88a4b4499267a0f3caacda11cef978adf1fe8b37f7f2ee6ae3fab237a5a6" -dependencies = [ - "alloy-primitives", - "async-trait", - "bytes", - "chrono", - "clap", - "convert_case 0.6.0", - "dashmap", - "derive_more", - "eyre", - "futures", - "hex", - "itertools 0.12.1", - "nago-wss", - "nagoya", - "parking_lot_lite_hack", - "serde", - "serde_json", - "serde_path_to_error", - "tracing", - "tracing-appender", - "tracing-subscriber", - "urlencoding", -] - -[[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 = "flexpect" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91463e4ffff882636c3bf769011bd013857c57c9485cd0e8f3be0cace23ef7db" -dependencies = [ - "rustversion", -] - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[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", - "libc", - "wasi", -] - -[[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", - "libc", - "r-efi 6.0.0", -] - -[[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 = "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 = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "hybrid-array" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" -dependencies = [ - "typenum", -] - -[[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 = "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 = "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 = "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.1", -] - -[[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 = "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 = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[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 = "nago-wss" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff98af5c923e68ee668fceb0c8fbe0ae6051b72fb8674949855455f558c18bd" -dependencies = [ - "bytes", - "libc", - "nagoya", - "ps-simdutf8", -] - -[[package]] -name = "nagoya" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9081694cf4bb0358e2f3a7730022cb916da76a64a248b074464ca2d8eddb4ba3" -dependencies = [ - "async-task", - "bytes", - "futures-io", - "futures-util", - "libc", - "ps-st3", - "spin", -] - -[[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 = "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_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 = "parking_lot_lite_hack" -version = "0.12.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4335701c28bc0ae2ca096e9693277a1e638c767472c41e41a3d22d0affd49508" -dependencies = [ - "lock_api", - "parking_lot_lite_hack_core", -] - -[[package]] -name = "parking_lot_lite_hack_core" -version = "0.9.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67f4481ce4fb7c3e3e4b602b0aca8f207e4dd89e3b7873ea2b3f57f158b5dc0" -dependencies = [ - "cfg-if", - "libc", - "smallvec", - "windows-link", - "windows-sys 0.60.2", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[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-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 = "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 = "ps-simdutf8" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4728371618f944f51f8ad94cd42963dd9a2b78a23689a1819b47ac0e3b060dda" -dependencies = [ - "cpufeatures 0.3.1", - "flexpect", - "rustversion", -] - -[[package]] -name = "ps-st3" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a78c6922d75f35bd19b6904f10f394c59104f70d1e87f4519d3e62fc04f02ac6" -dependencies = [ - "atomic-wait", - "crossbeam-queue", - "crossbeam-utils", - "spin", -] - -[[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_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_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 = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac", - "subtle", -] - -[[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 = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - -[[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_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 = "spin" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[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 = "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 = "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", - "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-tungstenite" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" -dependencies = [ - "futures-util", - "log", - "tokio", - "tungstenite", -] - -[[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-io", - "futures-sink", - "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 = "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-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 = "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", - "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 = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - -[[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 = "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-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 = "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-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.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -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.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[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 = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - -[[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 = "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 = "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 d1bfdc2..b0bf427 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,11 +11,11 @@ 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.3", default-features = false, features = ["framed-transport"] } +endpoint-libs = { version = "^3", default-features = false, features = ["framed-transport"] } eyre = "0.6" futures = "0.3" serde = { version = "1", features = ["derive"] } diff --git a/crates/proxy/Cargo.toml b/crates/proxy/Cargo.toml index dcba5a5..086a4df 100644 --- a/crates/proxy/Cargo.toml +++ b/crates/proxy/Cargo.toml @@ -10,7 +10,7 @@ repository.workspace = true [dependencies] tokio-util = { version = "^0.7", features = ["compat"] } -agent-abstraction = "0.4.21" +agent-abstraction = "^0.4" agency-proxy-protocol.workspace = true async-trait.workspace = true clap.workspace = true From d7a80a3dd37623a5715112c4af6f96c4e056f64e Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 23 Sep 2026 21:10:12 +0700 Subject: [PATCH 5/5] Run on nagoya, with the reactor passed in tokio and tokio-util are gone from every crate, tests included, and so is tokio-tungstenite: the WebSocket tests drive the server with endpoint-libs' own nagoya client. Sockets are nagoya's, framed with NagoyaStream and the neutral framing, so no compat layer sits in between. tokio's broadcast and watch become two small local types, broadcast and Flag, with the semantics the code relied on (lag reporting, close on last sender, cancel-safe recv). There is no global reactor. main is the composition root: it registers SIGTERM and SIGINT before any thread exists, which a Linux signalfd needs, then creates the one Reactor, keeps it for the life of the process, and passes its Handle to the server, the registry and agent-abstraction. Every test starts its own. Public changes: Client::connect, ProxyServer::bind and bind_with_registry take the Handle; RuntimeRegistry::new(handle) replaces Default, which could only have been honest by creating a reactor; subscribe and Attachment::events return the local broadcast receiver. agent-abstraction moves to ^0.5, the release that takes the Handle too. --- Cargo.toml | 8 +- crates/client/Cargo.toml | 3 +- crates/client/src/broadcast.rs | 262 +++++++ crates/client/src/lib.rs | 113 ++- crates/proxy/Cargo.toml | 9 +- crates/proxy/src/broadcast.rs | 286 ++++++++ crates/proxy/src/config.rs | 7 +- crates/proxy/src/flag.rs | 47 ++ crates/proxy/src/lib.rs | 400 ++++++++--- crates/proxy/src/main.rs | 170 +++-- crates/proxy/src/runtime.rs | 289 +++++--- crates/proxy/src/web.rs | 57 +- crates/proxy/tests/local_socket.rs | 1037 ++++++++++++++------------- crates/proxy/tests/websocket_mcp.rs | 758 +++++++++----------- 14 files changed, 2212 insertions(+), 1234 deletions(-) create mode 100644 crates/client/src/broadcast.rs create mode 100644 crates/proxy/src/broadcast.rs create mode 100644 crates/proxy/src/flag.rs diff --git a/Cargo.toml b/Cargo.toml index b0bf427..25156ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,13 +15,17 @@ 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", "signal", "sync", "time"] } [profile.release] strip = true diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index e5ae2d1..a3e53a2 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -9,10 +9,9 @@ homepage.workspace = true repository.workspace = true [dependencies] -tokio-util = { version = "^0.7", features = ["compat"] } 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 6c01295..44bbfe5 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -1,19 +1,24 @@ +pub mod broadcast; + use agency_proxy_protocol::{ ClientFrame, ClientMessage, MAX_FRAME_BYTES, PROTOCOL_VERSION, ProtocolVersion, ServerFrame, ServerResponse, }; -// endpoint-libs 3.2 removed the tokio flavour of the transport; the neutral -// framing takes a `futures_io` stream, so a tokio `UnixStream` bridges through -// tokio-util's `compat`. -use endpoint_libs::libs::ws::{WireMessage, transport::framed::framed_json_neutral_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 tokio_util::compat::TokioAsyncReadCompatExt; +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 { @@ -34,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_neutral_with_max_frame(stream.compat(), 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, @@ -81,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)? } @@ -90,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 @@ -101,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 086a4df..46ce0d9 100644 --- a/crates/proxy/Cargo.toml +++ b/crates/proxy/Cargo.toml @@ -9,24 +9,25 @@ homepage.workspace = true repository.workspace = true [dependencies] -tokio-util = { version = "^0.7", features = ["compat"] } -agent-abstraction = "^0.4" +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 b2f92eb..ce54d71 100644 --- a/crates/proxy/src/config.rs +++ b/crates/proxy/src/config.rs @@ -45,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) 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 5dfdf51..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,28 @@ use agency_proxy_protocol::{ Capability, ClientFrame, ClientMessage, ErrorCode, MAX_FRAME_BYTES, PROTOCOL_VERSION, RunId, ServerFrame, ServerResponse, ShutdownMode, }; -// endpoint-libs 3.2 removed the tokio flavour of the transport; the neutral -// framing takes a `futures_io` stream, so a tokio `UnixStream` bridges through -// tokio-util's `compat`. -use endpoint_libs::libs::ws::{WireMessage, transport::framed::framed_json_neutral_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}; -use tokio_util::compat::TokioAsyncReadCompatExt; pub use config::{ConfigError, ConnectionConfig, ProxyConfig}; pub use runtime::{Attachment, RuntimeError, RuntimeRegistry, SequencedEvent}; @@ -42,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)] @@ -55,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(), }) } @@ -83,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()), @@ -129,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(()) } @@ -143,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_neutral_with_max_frame(stream.compat(), 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(()); }; @@ -191,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( @@ -223,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 7456c6f..e96552d 100644 --- a/crates/proxy/src/main.rs +++ b/crates/proxy/src/main.rs @@ -2,7 +2,10 @@ use agency_proxy::{ 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)] @@ -15,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}"); @@ -34,42 +45,27 @@ async fn main() -> ExitCode { }, }, }; - let registry = RuntimeRegistry::default(); - let result = match config.connection { - ConnectionConfig::Unix { socket } => serve_unix(socket, registry).await, - ConnectionConfig::WebSocket { - address, - authentication_key, - allowed_origins, - } => match termination() { - Err(error) => Err(format!("could not register SIGTERM and SIGINT: {error}")), - Ok(terminated) => { - 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, where tokio's signal - // driver does not reach, so the signal is waited for here and - // forwarded through a oneshot, which needs no runtime to await. - let (stop, stopped) = tokio::sync::oneshot::channel::<()>(); - tokio::spawn(async move { - terminated.await; - let _ = stop.send(()); - }); - serve_websocket( - registry, - WebSocketConfig { - address, - authentication_key, - allowed_origins, - }, - async move { - let _ = stopped.await; - }, - ) - .await - .map_err(|error| error.to_string()) + 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) => { @@ -83,20 +79,98 @@ async fn main() -> ExitCode { /// /// Both are registered before this returns, so a signal that arrives while the /// server is still binding is not lost to the default action. -fn termination() -> std::io::Result> { - use tokio::signal::unix::{SignalKind, signal}; - let mut terminate = signal(SignalKind::terminate())?; - let mut interrupt = signal(SignalKind::interrupt())?; - Ok(async move { - tokio::select! { - _ = terminate.recv() => {} - _ = interrupt.recv() => {} +/// +/// 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, + } => { + 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, + }, + 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()) } - }) + } } -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 e304e3f..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,20 +131,33 @@ pub enum RuntimeError { } impl RuntimeRegistry { - /// Run `future` on the runtime that owns this registry's provider tasks. + /// 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 tokio's, - /// such as an endpoint-libs WebSocket handler, which is polled on that - /// server's reactor and can neither `spawn_local` nor reach tokio's timers. - pub(crate) fn spawn(&self, future: F) -> tokio::task::JoinHandle + /// 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, { - self.executor.spawn(future) + 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(); @@ -138,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, @@ -158,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(); @@ -214,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> { @@ -225,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, @@ -263,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) => { @@ -304,7 +344,7 @@ impl RuntimeRegistry { .await } } - }); + })); Ok(()) } @@ -388,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; @@ -420,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())) } @@ -575,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 { @@ -716,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 23171f4..2cdd61d 100644 --- a/crates/proxy/src/web.rs +++ b/crates/proxy/src/web.rs @@ -17,7 +17,11 @@ 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, @@ -68,8 +72,9 @@ impl AuthController for Authentication { /// `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, so it must not need tokio: a oneshot -/// receiver is the intended shape. +/// 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, @@ -80,11 +85,11 @@ where { let drain_registry = registry.clone(); // The server blocks the thread it runs on: it owns a reactor and polls it - // until `stop`. On a tokio worker that held the worker for the life of the - // process, and on a current-thread runtime, which every test uses, it held - // the only one. It gets a thread of its own, and this future waits for its - // answer without blocking. - let (answer, answered) = tokio::sync::oneshot::channel(); + // 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 || { @@ -159,9 +164,15 @@ where 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, } @@ -170,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)) { @@ -414,11 +425,11 @@ impl RequestHandler for MethodSubscribeRun { let registry = self.registry.clone(); let toolbox = TOOLBOX.with(Arc::clone); let connection_id = ctx.connection_id; - // On the registry's runtime, not the connection's. endpoint-libs 3 polls - // a handler in place on the connection task and has no spawner, and - // `spawn_local` needs a tokio LocalSet this thread no longer has. The - // forwarder needs nothing thread-bound: the toolbox is `Send + Sync`. - let task = self.registry.spawn(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, @@ -430,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, @@ -447,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 { @@ -460,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; }; @@ -475,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 01e2ec4..743bc92 100644 --- a/crates/proxy/tests/local_socket.rs +++ b/crates/proxy/tests/local_socket.rs @@ -4,562 +4,611 @@ use agency_proxy_protocol::{ ClientFrame, ClientMessage, ErrorCode, MAX_FRAME_BYTES, RunEvent, RunId, RunRequest, ServerFrame, ServerResponse, ShutdownMode, }; -// endpoint-libs 3.2 removed the tokio flavour of the transport; the neutral -// framing takes a `futures_io` stream, so a tokio `UnixStream` bridges through -// tokio-util's `compat`. -use endpoint_libs::libs::ws::{WireMessage, transport::framed::framed_json_neutral_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; -use tokio_util::compat::TokioAsyncReadCompatExt; -#[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_neutral_with_max_frame(stream.compat(), 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 aff6d4a..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,77 +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()], - }, - // 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 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, @@ -101,94 +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()], - }, - // 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 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, @@ -198,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, @@ -279,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, @@ -364,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, @@ -409,118 +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()], - }, - // 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 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":{ @@ -538,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(); + }); }