diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 51d4dcd85..834b1acad 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -608,6 +608,7 @@ "addr2line_0.25.1": "{\"dependencies\":[{\"name\":\"alloc\",\"optional\":true,\"package\":\"rustc-std-workspace-alloc\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"backtrace\",\"req\":\"^0.3.13\"},{\"features\":[\"wrap_help\"],\"name\":\"clap\",\"optional\":true,\"req\":\"^4.3.21\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"cpp_demangle\",\"optional\":true,\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7.0\"},{\"default_features\":false,\"name\":\"fallible-iterator\",\"optional\":true,\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"findshlibs\",\"req\":\"^0.10\"},{\"default_features\":false,\"features\":[\"read\"],\"name\":\"gimli\",\"req\":\"^0.32.0\"},{\"kind\":\"dev\",\"name\":\"libtest-mimic\",\"req\":\"^0.8.1\"},{\"name\":\"memmap2\",\"optional\":true,\"req\":\"^0.9.4\"},{\"default_features\":false,\"features\":[\"read\",\"compression\"],\"name\":\"object\",\"optional\":true,\"req\":\"^0.37.0\"},{\"name\":\"rustc-demangle\",\"optional\":true,\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"smallvec\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"typed-arena\",\"optional\":true,\"req\":\"^2\"}],\"features\":{\"all\":[\"bin\",\"wasm\"],\"bin\":[\"loader\",\"rustc-demangle\",\"cpp_demangle\",\"fallible-iterator\",\"smallvec\",\"dep:clap\"],\"cargo-all\":[],\"default\":[\"rustc-demangle\",\"cpp_demangle\",\"loader\",\"fallible-iterator\",\"smallvec\"],\"loader\":[\"std\",\"dep:object\",\"dep:memmap2\",\"dep:typed-arena\"],\"rustc-dep-of-std\":[\"core\",\"alloc\",\"gimli/rustc-dep-of-std\"],\"std\":[\"gimli/std\"],\"wasm\":[\"object/wasm\"]}}", "adler2_2.0.1": "{\"dependencies\":[{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"}],\"features\":{\"default\":[\"std\"],\"rustc-dep-of-std\":[\"core\"],\"std\":[]}}", "aead_0.5.2": "{\"dependencies\":[{\"default_features\":false,\"name\":\"arrayvec\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"blobby\",\"optional\":true,\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"bytes\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"crypto-common\",\"req\":\"^0.1.4\"},{\"default_features\":false,\"name\":\"generic-array\",\"req\":\"^0.14\"},{\"default_features\":false,\"name\":\"heapless\",\"optional\":true,\"req\":\"^0.7\"}],\"features\":{\"alloc\":[],\"default\":[\"rand_core\"],\"dev\":[\"blobby\"],\"getrandom\":[\"crypto-common/getrandom\",\"rand_core\"],\"rand_core\":[\"crypto-common/rand_core\"],\"std\":[\"alloc\",\"crypto-common/std\"],\"stream\":[]}}", + "aes-gcm_0.10.3": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aead\",\"req\":\"^0.5\"},{\"default_features\":false,\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"aead\",\"req\":\"^0.5\"},{\"name\":\"aes\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"cipher\",\"req\":\"^0.4\"},{\"name\":\"ctr\",\"req\":\"^0.9\"},{\"default_features\":false,\"name\":\"ghash\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"aead/alloc\"],\"arrayvec\":[\"aead/arrayvec\"],\"default\":[\"aes\",\"alloc\",\"getrandom\"],\"getrandom\":[\"aead/getrandom\",\"rand_core\"],\"heapless\":[\"aead/heapless\"],\"rand_core\":[\"aead/rand_core\"],\"std\":[\"aead/std\",\"alloc\"],\"stream\":[\"aead/stream\"]}}", "aes_0.8.4": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"cipher\",\"req\":\"^0.4.2\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"cipher\",\"req\":\"^0.4.2\"},{\"name\":\"cpufeatures\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_arch = \\\"aarch64\\\", target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"aarch64\"],\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.5.6\",\"target\":\"cfg(all(aes_armv8, target_arch = \\\"aarch64\\\"))\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.6.0\",\"target\":\"cfg(not(all(aes_armv8, target_arch = \\\"aarch64\\\")))\"}],\"features\":{\"hazmat\":[]}}", "age-core_0.11.0": "{\"dependencies\":[{\"name\":\"base64\",\"req\":\"^0.21\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"chacha20poly1305\",\"req\":\"^0.10\"},{\"name\":\"cookie-factory\",\"req\":\"^0.3.1\"},{\"name\":\"hkdf\",\"req\":\"^0.12\"},{\"name\":\"io_tee\",\"req\":\"^0.1.1\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"nom\",\"req\":\"^7\"},{\"name\":\"rand\",\"req\":\"^0.8\"},{\"name\":\"secrecy\",\"req\":\"^0.10\"},{\"name\":\"sha2\",\"req\":\"^0.10\"},{\"name\":\"tempfile\",\"optional\":true,\"req\":\"^3.2.0\"}],\"features\":{\"plugin\":[\"tempfile\"],\"unstable\":[]}}", "age_0.11.2": "{\"dependencies\":[{\"name\":\"aes\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"aes-gcm\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"age-core\",\"req\":\"^0.11.0\"},{\"name\":\"base64\",\"req\":\"^0.21\"},{\"name\":\"bcrypt-pbkdf\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"bech32\",\"req\":\"^0.9\"},{\"name\":\"cbc\",\"optional\":true,\"req\":\"^0.1\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"chacha20poly1305\",\"req\":\"^0.10\"},{\"features\":[\"alloc\"],\"name\":\"cipher\",\"optional\":true,\"req\":\"^0.4.3\"},{\"default_features\":false,\"name\":\"console\",\"optional\":true,\"req\":\"^0.15\"},{\"name\":\"cookie-factory\",\"req\":\"^0.3.1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"criterion-cycles-per-byte\",\"req\":\"^0.6\",\"target\":\"cfg(any(target_arch = \\\"x86\\\", target_arch = \\\"x86_64\\\"))\"},{\"name\":\"ctr\",\"optional\":true,\"req\":\"^0.9\"},{\"name\":\"curve25519-dalek\",\"optional\":true,\"req\":\"^4\"},{\"name\":\"futures\",\"optional\":true,\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"futures-test\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4\"},{\"name\":\"hmac\",\"req\":\"^0.12\"},{\"features\":[\"fluent-system\"],\"name\":\"i18n-embed\",\"req\":\"^0.15\"},{\"features\":[\"fluent-system\",\"desktop-requester\"],\"kind\":\"dev\",\"name\":\"i18n-embed\",\"req\":\"^0.15\"},{\"name\":\"i18n-embed-fl\",\"req\":\"^0.9\"},{\"name\":\"is-terminal\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"lazy_static\",\"req\":\"^1\"},{\"name\":\"memchr\",\"optional\":true,\"req\":\"^2.5\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"nom\",\"req\":\"^7\"},{\"name\":\"num-traits\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"pin-project\",\"req\":\"^1\"},{\"name\":\"pinentry\",\"optional\":true,\"req\":\"^0.6\"},{\"features\":[\"criterion\",\"flamegraph\"],\"kind\":\"dev\",\"name\":\"pprof\",\"req\":\"^0.13\",\"target\":\"cfg(unix)\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"name\":\"rand\",\"req\":\"^0.8\"},{\"name\":\"rpassword\",\"optional\":true,\"req\":\"^7\"},{\"default_features\":false,\"name\":\"rsa\",\"optional\":true,\"req\":\"^0.9\"},{\"name\":\"rust-embed\",\"req\":\"^8\"},{\"default_features\":false,\"name\":\"scrypt\",\"req\":\"^0.11\"},{\"name\":\"sha2\",\"req\":\"^0.10\"},{\"name\":\"subtle\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"test-case\",\"req\":\"^3\"},{\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"Window\",\"Performance\"],\"name\":\"web-sys\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"which\",\"optional\":true,\"req\":\"^4\",\"target\":\"cfg(any(unix, windows))\"},{\"name\":\"wsl\",\"optional\":true,\"req\":\"^0.1\",\"target\":\"cfg(any(unix, windows))\"},{\"features\":[\"static_secrets\"],\"name\":\"x25519-dalek\",\"req\":\"^2\"},{\"name\":\"zeroize\",\"req\":\"^1\"}],\"features\":{\"armor\":[],\"async\":[\"futures\",\"memchr\"],\"cli-common\":[\"console\",\"is-terminal\",\"pinentry\",\"rpassword\"],\"default\":[],\"plugin\":[\"age-core/plugin\",\"which\",\"wsl\"],\"ssh\":[\"aes\",\"aes-gcm\",\"bcrypt-pbkdf\",\"cbc\",\"cipher\",\"ctr\",\"curve25519-dalek\",\"num-traits\",\"rsa\"],\"unstable\":[\"age-core/unstable\"]}}", @@ -757,6 +758,7 @@ "clap_derive_4.6.0": "{\"dependencies\":[{\"name\":\"anstyle\",\"optional\":true,\"req\":\"^1.0.13\"},{\"name\":\"heck\",\"req\":\"^0.5.0\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.106\"},{\"default_features\":false,\"name\":\"pulldown-cmark\",\"optional\":true,\"req\":\"^0.13.1\"},{\"name\":\"quote\",\"req\":\"^1.0.45\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0.117\"}],\"features\":{\"debug\":[],\"default\":[],\"deprecated\":[],\"raw-deprecated\":[\"deprecated\"],\"unstable-markdown\":[\"dep:pulldown-cmark\",\"dep:anstyle\"],\"unstable-v5\":[\"deprecated\"]}}", "clap_lex_1.0.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"automod\",\"req\":\"^1.0.14\"}],\"features\":{}}", "clap_lex_1.1.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"automod\",\"req\":\"^1.0.16\"}],\"features\":{}}", + "clatter_2.2.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"aes\"],\"name\":\"aes-gcm\",\"optional\":true,\"req\":\"^0.10.3\"},{\"default_features\":false,\"features\":[\"zeroize\"],\"name\":\"arrayvec\",\"req\":\"^0.7.6\"},{\"default_features\":false,\"name\":\"blake2\",\"optional\":true,\"req\":\"^0.10.6\"},{\"default_features\":false,\"features\":[\"rand_core\"],\"name\":\"chacha20poly1305\",\"optional\":true,\"req\":\"^0.10.1\"},{\"default_features\":false,\"name\":\"displaydoc\",\"req\":\"^0.2.5\"},{\"name\":\"getrandom\",\"optional\":true,\"req\":\"^0.3.3\"},{\"default_features\":false,\"features\":[\"zeroize\"],\"name\":\"ml-kem\",\"optional\":true,\"req\":\"^0.2.1\"},{\"default_features\":false,\"name\":\"pqcrypto-mlkem\",\"optional\":true,\"req\":\"^0.1.1\"},{\"default_features\":false,\"name\":\"pqcrypto-traits\",\"optional\":true,\"req\":\"^0.3.5\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"default_features\":false,\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10.9\"},{\"default_features\":false,\"name\":\"thiserror-no-std\",\"req\":\"^2.0.2\"},{\"default_features\":false,\"features\":[\"static_secrets\",\"zeroize\"],\"name\":\"x25519-dalek\",\"optional\":true,\"req\":\"^2.0.1\"},{\"default_features\":false,\"features\":[\"zeroize_derive\"],\"name\":\"zeroize\",\"req\":\"^1.8.1\"}],\"features\":{\"alloc\":[],\"core\":[\"use-aes-gcm\",\"use-chacha20poly1305\",\"use-sha\",\"use-blake2\",\"use-25519\",\"use-rust-crypto-ml-kem\"],\"default\":[\"std\",\"use-aes-gcm\",\"use-chacha20poly1305\",\"use-sha\",\"use-blake2\",\"use-25519\",\"use-pqclean-ml-kem\",\"use-rust-crypto-ml-kem\"],\"getrandom\":[\"dep:getrandom\"],\"std\":[\"alloc\",\"sha2/std\",\"blake2/std\",\"aes-gcm/std\",\"chacha20poly1305/std\",\"ml-kem/std\",\"zeroize/std\",\"getrandom\"],\"use-25519\":[\"x25519-dalek\"],\"use-aes-gcm\":[\"aes-gcm\"],\"use-blake2\":[\"blake2\"],\"use-chacha20poly1305\":[\"chacha20poly1305\"],\"use-pqclean-ml-kem\":[\"pqcrypto-mlkem\",\"pqcrypto-traits\",\"getrandom\"],\"use-rust-crypto-ml-kem\":[\"ml-kem\"],\"use-sha\":[\"sha2\"]}}", "clipboard-win_5.4.1": "{\"dependencies\":[{\"name\":\"error-code\",\"req\":\"^3\",\"target\":\"cfg(windows)\"},{\"name\":\"windows-win\",\"optional\":true,\"req\":\"^3\",\"target\":\"cfg(windows)\"}],\"features\":{\"monitor\":[\"windows-win\"],\"std\":[\"error-code/std\"]}}", "clru_0.6.3": "{\"dependencies\":[{\"name\":\"hashbrown\",\"req\":\"^0.16\"}],\"features\":{}}", "cmake_0.1.57": "{\"dependencies\":[{\"name\":\"cc\",\"req\":\"^1.2.46\"}],\"features\":{}}", @@ -813,6 +815,7 @@ "ctor-proc-macro_0.0.7": "{\"dependencies\":[],\"features\":{\"default\":[]}}", "ctor_0.6.3": "{\"dependencies\":[{\"name\":\"ctor-proc-macro\",\"optional\":true,\"req\":\"=0.0.7\"},{\"default_features\":false,\"name\":\"dtor\",\"optional\":true,\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"libc-print\",\"req\":\"^0.1.20\"}],\"features\":{\"__no_warn_on_missing_unsafe\":[\"dtor?/__no_warn_on_missing_unsafe\"],\"default\":[\"dtor\",\"proc_macro\",\"__no_warn_on_missing_unsafe\"],\"dtor\":[\"dep:dtor\"],\"proc_macro\":[\"dep:ctor-proc-macro\",\"dtor?/proc_macro\"],\"used_linker\":[\"dtor?/used_linker\"]}}", "ctor_1.0.6": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"libc-print\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"link-section\",\"optional\":true,\"req\":\"^0.17.0\"},{\"features\":[\"ctor\"],\"name\":\"linktime-proc-macro\",\"optional\":true,\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"macrotest\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2\"}],\"features\":{\"default\":[\"std\",\"proc_macro\",\"priority\"],\"priority\":[\"dep:link-section\"],\"proc_macro\":[\"dep:linktime-proc-macro\"],\"std\":[]}}", + "ctr_0.9.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"aes\",\"req\":\"^0.8\"},{\"name\":\"cipher\",\"req\":\"^0.4.2\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"cipher\",\"req\":\"^0.4.2\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3.3\"},{\"kind\":\"dev\",\"name\":\"kuznyechik\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"magma\",\"req\":\"^0.8\"}],\"features\":{\"alloc\":[\"cipher/alloc\"],\"block-padding\":[\"cipher/block-padding\"],\"std\":[\"cipher/std\",\"alloc\"],\"zeroize\":[\"cipher/zeroize\"]}}", "ctutils_0.4.2": "{\"dependencies\":[{\"name\":\"cmov\",\"req\":\"^0.5.3\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.11\"},{\"default_features\":false,\"name\":\"subtle\",\"optional\":true,\"req\":\"^2\"}],\"features\":{\"alloc\":[],\"subtle\":[\"dep:subtle\"]}}", "curve25519-dalek-derive_0.1.1": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.66\"},{\"name\":\"quote\",\"req\":\"^1.0.31\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0.27\"}],\"features\":{}}", "curve25519-dalek_4.1.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1\"},{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"cpufeatures\",\"req\":\"^0.2.6\",\"target\":\"cfg(target_arch = \\\"x86_64\\\")\"},{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"name\":\"curve25519-dalek-derive\",\"req\":\"^0.1\",\"target\":\"cfg(all(not(curve25519_dalek_backend = \\\"fiat\\\"), not(curve25519_dalek_backend = \\\"serial\\\"), target_arch = \\\"x86_64\\\"))\"},{\"default_features\":false,\"name\":\"digest\",\"optional\":true,\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"ff\",\"optional\":true,\"req\":\"^0.13\"},{\"default_features\":false,\"name\":\"fiat-crypto\",\"req\":\"^0.2.1\",\"target\":\"cfg(curve25519_dalek_backend = \\\"fiat\\\")\"},{\"default_features\":false,\"name\":\"group\",\"optional\":true,\"req\":\"^0.13\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.2\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.6.4\"},{\"default_features\":false,\"features\":[\"getrandom\"],\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"kind\":\"build\",\"name\":\"rustc_version\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2.3.0\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"zeroize?/alloc\"],\"default\":[\"alloc\",\"precomputed-tables\",\"zeroize\"],\"group\":[\"dep:group\",\"rand_core\"],\"group-bits\":[\"group\",\"ff/bits\"],\"legacy_compatibility\":[],\"precomputed-tables\":[]}}", @@ -965,6 +968,7 @@ "getrandom_0.2.17": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"compiler_builtins\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0\"},{\"name\":\"js-sys\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(all(any(target_arch = \\\"wasm32\\\", target_arch = \\\"wasm64\\\"), target_os = \\\"unknown\\\"))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(unix)\"},{\"default_features\":false,\"name\":\"wasi\",\"req\":\"^0.11\",\"target\":\"cfg(target_os = \\\"wasi\\\")\"},{\"default_features\":false,\"name\":\"wasm-bindgen\",\"optional\":true,\"req\":\"^0.2.62\",\"target\":\"cfg(all(any(target_arch = \\\"wasm32\\\", target_arch = \\\"wasm64\\\"), target_os = \\\"unknown\\\"))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.18\",\"target\":\"cfg(all(any(target_arch = \\\"wasm32\\\", target_arch = \\\"wasm64\\\"), target_os = \\\"unknown\\\"))\"}],\"features\":{\"custom\":[],\"js\":[\"wasm-bindgen\",\"js-sys\"],\"linux_disable_fallback\":[],\"rdrand\":[],\"rustc-dep-of-std\":[\"compiler_builtins\",\"core\",\"libc/rustc-dep-of-std\",\"wasi/rustc-dep-of-std\"],\"std\":[],\"test-in-browser\":[]}}", "getrandom_0.3.4": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"js-sys\",\"optional\":true,\"req\":\"^0.3.77\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"), target_feature = \\\"atomics\\\"))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(all(any(target_os = \\\"linux\\\", target_os = \\\"android\\\"), not(any(all(target_os = \\\"linux\\\", target_env = \\\"\\\"), getrandom_backend = \\\"custom\\\", getrandom_backend = \\\"linux_raw\\\", getrandom_backend = \\\"rdrand\\\", getrandom_backend = \\\"rndr\\\"))))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(any(target_os = \\\"dragonfly\\\", target_os = \\\"freebsd\\\", target_os = \\\"hurd\\\", target_os = \\\"illumos\\\", target_os = \\\"cygwin\\\", all(target_os = \\\"horizon\\\", target_arch = \\\"arm\\\")))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(any(target_os = \\\"haiku\\\", target_os = \\\"redox\\\", target_os = \\\"nto\\\", target_os = \\\"aix\\\"))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(any(target_os = \\\"ios\\\", target_os = \\\"visionos\\\", target_os = \\\"watchos\\\", target_os = \\\"tvos\\\"))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(any(target_os = \\\"macos\\\", target_os = \\\"openbsd\\\", target_os = \\\"vita\\\", target_os = \\\"emscripten\\\"))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(target_os = \\\"netbsd\\\")\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(target_os = \\\"solaris\\\")\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(target_os = \\\"vxworks\\\")\"},{\"default_features\":false,\"name\":\"r-efi\",\"req\":\"^5.1\",\"target\":\"cfg(all(target_os = \\\"uefi\\\", getrandom_backend = \\\"efi_rng\\\"))\"},{\"default_features\":false,\"name\":\"wasip2\",\"req\":\"^1\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"wasi\\\", target_env = \\\"p2\\\"))\"},{\"default_features\":false,\"name\":\"wasm-bindgen\",\"optional\":true,\"req\":\"^0.2.98\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\")))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\")))\"}],\"features\":{\"std\":[],\"wasm_js\":[\"dep:wasm-bindgen\",\"dep:js-sys\"]}}", "getrandom_0.4.2": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"js-sys\",\"optional\":true,\"req\":\"^0.3.77\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"), target_feature = \\\"atomics\\\"))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(all(any(target_os = \\\"linux\\\", target_os = \\\"android\\\"), not(any(all(target_os = \\\"linux\\\", target_env = \\\"\\\"), getrandom_backend = \\\"custom\\\", getrandom_backend = \\\"linux_raw\\\", getrandom_backend = \\\"rdrand\\\", getrandom_backend = \\\"rndr\\\"))))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(any(target_os = \\\"dragonfly\\\", target_os = \\\"freebsd\\\", target_os = \\\"hurd\\\", target_os = \\\"illumos\\\", target_os = \\\"cygwin\\\", all(target_os = \\\"horizon\\\", target_arch = \\\"arm\\\")))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(any(target_os = \\\"haiku\\\", target_os = \\\"redox\\\", target_os = \\\"nto\\\", target_os = \\\"aix\\\"))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(any(target_os = \\\"ios\\\", target_os = \\\"visionos\\\", target_os = \\\"watchos\\\", target_os = \\\"tvos\\\"))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(any(target_os = \\\"macos\\\", target_os = \\\"openbsd\\\", target_os = \\\"vita\\\", target_os = \\\"emscripten\\\"))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(target_os = \\\"netbsd\\\")\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(target_os = \\\"solaris\\\")\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(target_os = \\\"vxworks\\\")\"},{\"default_features\":false,\"name\":\"r-efi\",\"req\":\"^6\",\"target\":\"cfg(all(target_os = \\\"uefi\\\", getrandom_backend = \\\"efi_rng\\\"))\"},{\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.10.0\"},{\"default_features\":false,\"name\":\"wasip2\",\"req\":\"^1\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"wasi\\\", target_env = \\\"p2\\\"))\"},{\"name\":\"wasip3\",\"req\":\"^0.4\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"wasi\\\", target_env = \\\"p3\\\"))\"},{\"default_features\":false,\"name\":\"wasm-bindgen\",\"optional\":true,\"req\":\"^0.2.98\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\")))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\")))\"}],\"features\":{\"std\":[],\"sys_rng\":[\"dep:rand_core\"],\"wasm_js\":[\"dep:wasm-bindgen\",\"dep:js-sys\"]}}", + "ghash_0.5.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3\"},{\"name\":\"opaque-debug\",\"req\":\"^0.3\"},{\"name\":\"polyval\",\"req\":\"^0.6.2\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"std\":[\"polyval/std\"]}}", "gif_0.14.1": "{\"dependencies\":[{\"name\":\"color_quant\",\"optional\":true,\"req\":\"^1.1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7.0\"},{\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"png\",\"req\":\"^0.18.0\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.10.0\"},{\"name\":\"weezl\",\"req\":\"^0.1.10\"}],\"features\":{\"color_quant\":[\"dep:color_quant\"],\"default\":[\"raii_no_panic\",\"std\",\"color_quant\"],\"raii_no_panic\":[],\"std\":[]}}", "gimli_0.32.3": "{\"dependencies\":[{\"name\":\"alloc\",\"optional\":true,\"package\":\"rustc-std-workspace-alloc\",\"req\":\"^1.0.0\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"name\":\"fallible-iterator\",\"optional\":true,\"req\":\"^0.3.0\"},{\"name\":\"indexmap\",\"optional\":true,\"req\":\"^2.0.0\"},{\"default_features\":false,\"name\":\"stable_deref_trait\",\"optional\":true,\"req\":\"^1.1.0\"},{\"kind\":\"dev\",\"name\":\"test-assembler\",\"req\":\"^0.1.3\"}],\"features\":{\"default\":[\"read-all\",\"write\"],\"endian-reader\":[\"read\",\"dep:stable_deref_trait\"],\"fallible-iterator\":[\"dep:fallible-iterator\"],\"read\":[\"read-core\"],\"read-all\":[\"read\",\"std\",\"fallible-iterator\",\"endian-reader\"],\"read-core\":[],\"rustc-dep-of-std\":[\"dep:core\",\"dep:alloc\"],\"std\":[\"fallible-iterator?/std\",\"stable_deref_trait?/std\"],\"write\":[\"dep:indexmap\"]}}", "gio-sys_0.21.5": "{\"dependencies\":[{\"name\":\"glib-sys\",\"req\":\"^0.21\"},{\"name\":\"gobject-sys\",\"req\":\"^0.21\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"shell-words\",\"req\":\"^1.0.0\"},{\"kind\":\"build\",\"name\":\"system-deps\",\"req\":\"^7\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"},{\"features\":[\"Win32_Networking_WinSock\"],\"name\":\"windows-sys\",\"req\":\">=0.52, <=0.61\",\"target\":\"cfg(windows)\"}],\"features\":{\"v2_58\":[],\"v2_60\":[\"v2_58\"],\"v2_62\":[\"v2_60\"],\"v2_64\":[\"v2_62\"],\"v2_66\":[\"v2_64\"],\"v2_68\":[\"v2_66\"],\"v2_70\":[\"v2_68\"],\"v2_72\":[\"v2_70\"],\"v2_74\":[\"v2_72\"],\"v2_76\":[\"v2_74\"],\"v2_78\":[\"v2_76\"],\"v2_80\":[\"v2_78\"],\"v2_82\":[\"v2_80\"],\"v2_84\":[\"v2_82\"],\"v2_86\":[\"v2_84\"]}}", @@ -1079,6 +1083,7 @@ "http_1.4.0": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"name\":\"itoa\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.0\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "httparse_1.10.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.5\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "httpdate_1.0.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"}],\"features\":{}}", + "hybrid-array_0.2.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"bytemuck\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"const-generics\"],\"name\":\"typenum\",\"req\":\"^1.17\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.8\"}],\"features\":{\"extra-sizes\":[]}}", "hybrid-array_0.4.12": "{\"dependencies\":[{\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"name\":\"bytemuck\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"ctutils\",\"optional\":true,\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"const-generics\"],\"name\":\"subtle\",\"optional\":true,\"req\":\"^2\"},{\"features\":[\"const-generics\"],\"name\":\"typenum\",\"req\":\"^1.20\"},{\"features\":[\"derive\"],\"name\":\"zerocopy\",\"optional\":true,\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.8\"}],\"features\":{\"alloc\":[],\"extra-sizes\":[]}}", "hyper-rustls_0.27.7": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"http\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"hyper\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"client-legacy\",\"tokio\"],\"name\":\"hyper-util\",\"req\":\"^0.1\"},{\"default_features\":false,\"features\":[\"server-auto\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.4\"},{\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"rustls\",\"req\":\"^0.23\"},{\"default_features\":false,\"features\":[\"tls12\"],\"kind\":\"dev\",\"name\":\"rustls\",\"req\":\"^0.23\"},{\"name\":\"rustls-native-certs\",\"optional\":true,\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rustls-pemfile\",\"req\":\"^2\"},{\"name\":\"rustls-platform-verifier\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"tokio\",\"req\":\"^1.0\"},{\"features\":[\"io-std\",\"macros\",\"net\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"tokio-rustls\",\"req\":\"^0.26\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"aws-lc-rs\":[\"rustls/aws_lc_rs\"],\"default\":[\"native-tokio\",\"http1\",\"tls12\",\"logging\",\"aws-lc-rs\"],\"fips\":[\"aws-lc-rs\",\"rustls/fips\"],\"http1\":[\"hyper-util/http1\"],\"http2\":[\"hyper-util/http2\"],\"logging\":[\"log\",\"tokio-rustls/logging\",\"rustls/logging\"],\"native-tokio\":[\"rustls-native-certs\"],\"ring\":[\"rustls/ring\"],\"tls12\":[\"tokio-rustls/tls12\",\"rustls/tls12\"],\"webpki-tokio\":[\"webpki-roots\"]}}", "hyper-timeout_0.5.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"name\":\"hyper\",\"req\":\"^1.1\"},{\"features\":[\"http1\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.1\"},{\"kind\":\"dev\",\"name\":\"hyper-tls\",\"req\":\"^0.6\"},{\"features\":[\"client-legacy\",\"http1\"],\"name\":\"hyper-util\",\"req\":\"^0.1.10\"},{\"features\":[\"client-legacy\",\"http1\",\"server\",\"server-graceful\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1.10\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"name\":\"tokio\",\"req\":\"^1.35\"},{\"features\":[\"io-std\",\"io-util\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.35\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"}],\"features\":{}}", @@ -1168,6 +1173,8 @@ "js-sys_0.3.85": "{\"dependencies\":[{\"default_features\":false,\"name\":\"once_cell\",\"req\":\"^1.12\"},{\"default_features\":false,\"name\":\"wasm-bindgen\",\"req\":\"=0.2.108\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"wasm-bindgen/std\"]}}", "jsonptr_0.7.1": "{\"dependencies\":[{\"features\":[\"fancy\"],\"name\":\"miette\",\"optional\":true,\"req\":\"^7.4.0\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0.0\"},{\"features\":[\"alloc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.203\"},{\"features\":[\"alloc\"],\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.119\"},{\"name\":\"syn\",\"optional\":true,\"req\":\"^1.0.109\",\"target\":\"cfg(any())\"},{\"name\":\"toml\",\"optional\":true,\"req\":\"^0.8\"}],\"features\":{\"assign\":[],\"default\":[\"std\",\"serde\",\"json\",\"resolve\",\"assign\",\"delete\"],\"delete\":[\"resolve\"],\"json\":[\"dep:serde_json\",\"serde\"],\"miette\":[\"dep:miette\",\"std\"],\"resolve\":[],\"std\":[\"serde/std\",\"serde_json?/std\"],\"toml\":[\"dep:toml\",\"serde\",\"std\"]}}", "jsonwebtoken_9.3.1": "{\"dependencies\":[{\"name\":\"base64\",\"req\":\"^0.22\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\"))))\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\")))))\"},{\"name\":\"js-sys\",\"req\":\"^0.3\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"pem\",\"optional\":true,\"req\":\"^3\"},{\"features\":[\"std\"],\"name\":\"ring\",\"req\":\"^0.17.4\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"std\",\"wasm32_unknown_unknown_js\"],\"name\":\"ring\",\"req\":\"^0.17.4\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"simple_asn1\",\"optional\":true,\"req\":\"^0.6\"},{\"features\":[\"wasm-bindgen\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\"))))\"},{\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\")))))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.1\"}],\"features\":{\"default\":[\"use_pem\"],\"use_pem\":[\"pem\",\"simple_asn1\"]}}", + "keccak_0.1.6": "{\"dependencies\":[{\"name\":\"cpufeatures\",\"req\":\"^0.2\",\"target\":\"cfg(target_arch = \\\"aarch64\\\")\"}],\"features\":{\"asm\":[],\"no_unroll\":[],\"simd\":[]}}", + "kem_0.3.0-pre.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"hpke\",\"req\":\"^0.10\"},{\"features\":[\"ecdsa\"],\"kind\":\"dev\",\"name\":\"p256\",\"req\":\"^0.9\"},{\"default_features\":false,\"features\":[\"pqcrypto-saber\"],\"kind\":\"dev\",\"name\":\"pqcrypto\",\"req\":\"^0.15\"},{\"kind\":\"dev\",\"name\":\"pqcrypto-traits\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"x3dh-ke\",\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"zeroize\",\"req\":\"^1.7\"}],\"features\":{}}", "keyring_3.6.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.22\"},{\"name\":\"byteorder\",\"optional\":true,\"req\":\"^1.2\",\"target\":\"cfg(target_os = \\\"windows\\\")\"},{\"features\":[\"derive\",\"wrap_help\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4\"},{\"name\":\"dbus-secret-service\",\"optional\":true,\"req\":\"^4.0.0-rc.1\",\"target\":\"cfg(target_os = \\\"openbsd\\\")\"},{\"name\":\"dbus-secret-service\",\"optional\":true,\"req\":\"^4.0.0-rc.2\",\"target\":\"cfg(target_os = \\\"linux\\\")\"},{\"name\":\"dbus-secret-service\",\"optional\":true,\"req\":\"^4.0.1\",\"target\":\"cfg(target_os = \\\"freebsd\\\")\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11.5\"},{\"kind\":\"dev\",\"name\":\"fastrand\",\"req\":\"^2\"},{\"features\":[\"std\"],\"name\":\"linux-keyutils\",\"optional\":true,\"req\":\"^0.2\",\"target\":\"cfg(target_os = \\\"linux\\\")\"},{\"name\":\"log\",\"req\":\"^0.4.22\"},{\"name\":\"openssl\",\"optional\":true,\"req\":\"^0.10.66\"},{\"kind\":\"dev\",\"name\":\"rpassword\",\"req\":\"^7\"},{\"kind\":\"dev\",\"name\":\"rprompt\",\"req\":\"^2\"},{\"name\":\"secret-service\",\"optional\":true,\"req\":\"^4\",\"target\":\"cfg(target_os = \\\"freebsd\\\")\"},{\"name\":\"secret-service\",\"optional\":true,\"req\":\"^4\",\"target\":\"cfg(target_os = \\\"linux\\\")\"},{\"name\":\"secret-service\",\"optional\":true,\"req\":\"^4\",\"target\":\"cfg(target_os = \\\"openbsd\\\")\"},{\"name\":\"security-framework\",\"optional\":true,\"req\":\"^2\",\"target\":\"cfg(target_os = \\\"ios\\\")\"},{\"name\":\"security-framework\",\"optional\":true,\"req\":\"^3\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"kind\":\"dev\",\"name\":\"whoami\",\"req\":\"^1.5\"},{\"features\":[\"Win32_Foundation\",\"Win32_Security_Credentials\"],\"name\":\"windows-sys\",\"optional\":true,\"req\":\"^0.60\",\"target\":\"cfg(target_os = \\\"windows\\\")\"},{\"name\":\"zbus\",\"optional\":true,\"req\":\"^4\",\"target\":\"cfg(target_os = \\\"freebsd\\\")\"},{\"name\":\"zbus\",\"optional\":true,\"req\":\"^4\",\"target\":\"cfg(target_os = \\\"linux\\\")\"},{\"name\":\"zbus\",\"optional\":true,\"req\":\"^4\",\"target\":\"cfg(target_os = \\\"openbsd\\\")\"},{\"name\":\"zeroize\",\"req\":\"^1.8.1\",\"target\":\"cfg(target_os = \\\"windows\\\")\"}],\"features\":{\"apple-native\":[\"dep:security-framework\"],\"async-io\":[\"zbus?/async-io\"],\"async-secret-service\":[\"dep:secret-service\",\"dep:zbus\"],\"crypto-openssl\":[\"dbus-secret-service?/crypto-openssl\",\"secret-service?/crypto-openssl\"],\"crypto-rust\":[\"dbus-secret-service?/crypto-rust\",\"secret-service?/crypto-rust\"],\"linux-native\":[\"dep:linux-keyutils\"],\"linux-native-async-persistent\":[\"linux-native\",\"async-secret-service\"],\"linux-native-sync-persistent\":[\"linux-native\",\"sync-secret-service\"],\"sync-secret-service\":[\"dep:dbus-secret-service\"],\"tokio\":[\"zbus?/tokio\"],\"vendored\":[\"dbus-secret-service?/vendored\",\"openssl?/vendored\"],\"windows-native\":[\"dep:windows-sys\",\"dep:byteorder\"]}}", "kqueue-sys_1.0.4": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^1.2.1\"},{\"name\":\"libc\",\"req\":\"^0.2.74\"}],\"features\":{}}", "kqueue_1.1.1": "{\"dependencies\":[{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"dhat\",\"req\":\"^0.3.2\"},{\"name\":\"kqueue-sys\",\"req\":\"^1.0.4\"},{\"name\":\"libc\",\"req\":\"^0.2.17\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.1.0\"}],\"features\":{}}", @@ -1230,6 +1237,7 @@ "mio_1.1.1": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"name\":\"libc\",\"req\":\"^0.2.178\",\"target\":\"cfg(target_os = \\\"hermit\\\")\"},{\"name\":\"libc\",\"req\":\"^0.2.178\",\"target\":\"cfg(target_os = \\\"wasi\\\")\"},{\"name\":\"libc\",\"req\":\"^0.2.178\",\"target\":\"cfg(unix)\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.8\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"name\":\"wasi\",\"req\":\"^0.11.0\",\"target\":\"cfg(target_os = \\\"wasi\\\")\"},{\"features\":[\"Wdk_Foundation\",\"Wdk_Storage_FileSystem\",\"Wdk_System_IO\",\"Win32_Foundation\",\"Win32_Networking_WinSock\",\"Win32_Storage_FileSystem\",\"Win32_Security\",\"Win32_System_IO\",\"Win32_System_WindowsProgramming\"],\"name\":\"windows-sys\",\"req\":\"^0.61\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"log\"],\"net\":[],\"os-ext\":[\"os-poll\",\"windows-sys/Win32_System_Pipes\",\"windows-sys/Win32_Security\"],\"os-poll\":[]}}", "mio_1.2.0": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"name\":\"libc\",\"req\":\"^0.2.183\",\"target\":\"cfg(any(unix, target_os = \\\"hermit\\\", target_os = \\\"wasi\\\"))\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.8\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"name\":\"wasi\",\"req\":\"^0.11.0\",\"target\":\"cfg(target_os = \\\"wasi\\\")\"},{\"features\":[\"Wdk_Foundation\",\"Wdk_Storage_FileSystem\",\"Wdk_System_IO\",\"Win32_Foundation\",\"Win32_Networking_WinSock\",\"Win32_Storage_FileSystem\",\"Win32_Security\",\"Win32_System_IO\",\"Win32_System_WindowsProgramming\"],\"name\":\"windows-sys\",\"req\":\"^0.61\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"log\"],\"net\":[],\"os-ext\":[\"os-poll\",\"windows-sys/Win32_System_Pipes\",\"windows-sys/Win32_Security\"],\"os-poll\":[]}}", "miow_0.6.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.0\"},{\"kind\":\"dev\",\"name\":\"socket2\",\"req\":\"^0.6.0\"},{\"features\":[\"Win32_Foundation\",\"Win32_Networking_WinSock\",\"Win32_Security\",\"Win32_Storage_FileSystem\",\"Win32_System_IO\",\"Win32_System_Pipes\",\"Win32_System_Threading\"],\"name\":\"windows-sys\",\"req\":\">=0.60, <=0.61\"}],\"features\":{}}", + "ml-kem_0.2.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.1\"},{\"features\":[\"rand_core\"],\"kind\":\"dev\",\"name\":\"crypto-common\",\"req\":\"^0.1.6\"},{\"features\":[\"serde\"],\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4.1\"},{\"features\":[\"extra-sizes\"],\"name\":\"hybrid-array\",\"req\":\"^0.2.0-rc.9\"},{\"name\":\"kem\",\"req\":\"=0.3.0-pre.0\"},{\"default_features\":false,\"features\":[\"num-bigint\"],\"kind\":\"dev\",\"name\":\"num-rational\",\"req\":\"^0.4.2\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"name\":\"rand_core\",\"req\":\"^0.6.4\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.208\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.125\"},{\"default_features\":false,\"name\":\"sha3\",\"req\":\"^0.10.8\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.8.1\"}],\"features\":{\"default\":[\"std\"],\"deterministic\":[],\"std\":[\"sha3/std\"],\"zeroize\":[\"dep:zeroize\"]}}", "moka_0.12.13": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"actix-rt\",\"req\":\"^2.8\"},{\"kind\":\"dev\",\"name\":\"ahash\",\"req\":\"^0.8.3\"},{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.19\"},{\"name\":\"async-lock\",\"optional\":true,\"req\":\"^3.3\"},{\"name\":\"crossbeam-channel\",\"req\":\"^0.5.15\"},{\"name\":\"crossbeam-epoch\",\"req\":\"^0.9.18\"},{\"name\":\"crossbeam-utils\",\"req\":\"^0.8.21\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10.0\"},{\"name\":\"equivalent\",\"req\":\"^1.0\"},{\"name\":\"event-listener\",\"optional\":true,\"req\":\"^5.3\"},{\"name\":\"futures-util\",\"optional\":true,\"req\":\"^0.3.17\"},{\"kind\":\"dev\",\"name\":\"getrandom\",\"req\":\"^0.2\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"loom\",\"req\":\"^0.7\",\"target\":\"cfg(moka_loom)\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1.7\"},{\"name\":\"parking_lot\",\"req\":\"^0.12\"},{\"name\":\"portable-atomic\",\"req\":\"^1.6\"},{\"name\":\"quanta\",\"optional\":true,\"req\":\"^0.12.2\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"default_features\":false,\"features\":[\"rustls-tls\"],\"kind\":\"dev\",\"name\":\"reqwest\",\"req\":\"^0.12\"},{\"name\":\"smallvec\",\"req\":\"^1.8\"},{\"name\":\"tagptr\",\"req\":\"^0.2\"},{\"features\":[\"fs\",\"io-util\",\"macros\",\"rt-multi-thread\",\"sync\",\"time\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.19\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0\",\"target\":\"cfg(trybuild)\"},{\"features\":[\"v4\"],\"name\":\"uuid\",\"req\":\"^1.1\"}],\"features\":{\"atomic64\":[],\"default\":[],\"future\":[\"async-lock\",\"event-listener\",\"futures-util\"],\"logging\":[\"log\"],\"quanta\":[\"dep:quanta\"],\"sync\":[],\"unstable-debug-counters\":[\"future\"]}}", "moxcms_0.7.11": "{\"dependencies\":[{\"name\":\"num-traits\",\"req\":\"^0.2\"},{\"name\":\"pxfm\",\"req\":\"^0.1.1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"}],\"features\":{\"avx\":[],\"avx512\":[],\"default\":[\"avx\",\"sse\",\"neon\"],\"neon\":[],\"options\":[],\"sse\":[]}}", "multimap_0.10.1": "{\"dependencies\":[{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"serde_impl\"],\"serde_impl\":[\"serde\"]}}", @@ -1336,6 +1344,7 @@ "png_0.18.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"approx\",\"req\":\"^0.5.1\"},{\"name\":\"bitflags\",\"req\":\"^2.0\"},{\"kind\":\"dev\",\"name\":\"byteorder\",\"req\":\"^1.5.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.0\"},{\"name\":\"crc32fast\",\"req\":\"^1.2.0\"},{\"default_features\":false,\"features\":[\"cargo_bench_support\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"fdeflate\",\"req\":\"^0.3.3\"},{\"name\":\"flate2\",\"req\":\"^1.0.35\"},{\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3\"},{\"features\":[\"simd\"],\"name\":\"miniz_oxide\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9.2\"}],\"features\":{\"benchmarks\":[],\"unstable\":[\"crc32fast/nightly\"],\"zlib-rs\":[\"flate2/zlib-rs\"]}}", "polling_3.11.0": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"concurrent-queue\",\"req\":\"^2.2.0\",\"target\":\"cfg(windows)\"},{\"kind\":\"dev\",\"name\":\"easy-parallel\",\"req\":\"^3.1.0\"},{\"kind\":\"dev\",\"name\":\"fastrand\",\"req\":\"^2.0.0\"},{\"name\":\"hermit-abi\",\"req\":\"^0.5.0\",\"target\":\"cfg(target_os = \\\"hermit\\\")\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.9\",\"target\":\"cfg(windows)\"},{\"default_features\":false,\"features\":[\"event\",\"fs\",\"pipe\",\"process\",\"std\",\"time\"],\"name\":\"rustix\",\"req\":\"^1.0.5\",\"target\":\"cfg(any(unix, target_os = \\\"fuchsia\\\", target_os = \\\"vxworks\\\"))\"},{\"kind\":\"dev\",\"name\":\"signal-hook\",\"req\":\"^0.3.17\",\"target\":\"cfg(all(unix, not(target_os=\\\"vita\\\")))\"},{\"kind\":\"dev\",\"name\":\"socket2\",\"req\":\"^0.6.0\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.37\"},{\"features\":[\"Wdk_Foundation\",\"Wdk_Storage_FileSystem\",\"Win32_Foundation\",\"Win32_Networking_WinSock\",\"Win32_Security\",\"Win32_Storage_FileSystem\",\"Win32_System_IO\",\"Win32_System_LibraryLoader\",\"Win32_System_Threading\",\"Win32_System_WindowsProgramming\"],\"name\":\"windows-sys\",\"req\":\"^0.61\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "poly1305_0.8.0": "{\"dependencies\":[{\"name\":\"cpufeatures\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3\"},{\"name\":\"opaque-debug\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"universal-hash\",\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"std\":[\"universal-hash/std\"]}}", + "polyval_0.6.2": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"cpufeatures\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_arch = \\\"aarch64\\\", target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3\"},{\"name\":\"opaque-debug\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"universal-hash\",\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"std\":[\"universal-hash/std\"]}}", "portable-atomic-util_0.2.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"build-context\",\"req\":\"^0.1\"},{\"default_features\":false,\"features\":[\"require-cas\"],\"name\":\"portable-atomic\",\"req\":\"^1.5.1\"}],\"features\":{\"alloc\":[],\"default\":[],\"std\":[\"alloc\"]}}", "portable-atomic-util_0.2.6": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"build-context\",\"req\":\"^0.1\"},{\"default_features\":false,\"features\":[\"require-cas\"],\"name\":\"portable-atomic\",\"req\":\"^1.5.1\"}],\"features\":{\"alloc\":[],\"default\":[],\"std\":[\"alloc\"]}}", "portable-atomic_1.13.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"build-context\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"crabgrind\",\"req\":\"^0.1\",\"target\":\"cfg(valgrind)\"},{\"name\":\"critical-section\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"crossbeam-utils\",\"req\":\"=0.8.16\"},{\"kind\":\"dev\",\"name\":\"fastrand\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"=0.2.163\",\"target\":\"cfg(unix)\"},{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.60\"},{\"kind\":\"dev\",\"name\":\"sptr\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Threading\"],\"kind\":\"dev\",\"name\":\"windows-sys\",\"req\":\"^0.61\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"fallback\"],\"disable-fiq\":[],\"fallback\":[],\"float\":[],\"force-amo\":[],\"require-cas\":[],\"s-mode\":[],\"std\":[],\"unsafe-assume-privileged\":[],\"unsafe-assume-single-core\":[]}}", @@ -1522,6 +1531,7 @@ "sha1_smol_1.0.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"openssl\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.4\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"alloc\":[],\"std\":[\"alloc\"]}}", "sha2_0.10.9": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"name\":\"cpufeatures\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_arch = \\\"aarch64\\\", target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\"},{\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.2.2\"},{\"name\":\"sha2-asm\",\"optional\":true,\"req\":\"^0.6.1\",\"target\":\"cfg(any(target_arch = \\\"aarch64\\\", target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\"}],\"features\":{\"asm\":[\"sha2-asm\"],\"asm-aarch64\":[\"asm\"],\"compress\":[],\"default\":[\"std\"],\"force-soft\":[],\"force-soft-compact\":[],\"loongarch64_asm\":[],\"oid\":[\"digest/oid\"],\"std\":[\"digest/std\"]}}", "sha2_0.11.0": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"cpufeatures\",\"req\":\"^0.3\",\"target\":\"cfg(any(target_arch = \\\"aarch64\\\", target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\"},{\"name\":\"digest\",\"req\":\"^0.11\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"digest\",\"req\":\"^0.11\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^1\"}],\"features\":{\"alloc\":[\"digest/alloc\"],\"default\":[\"alloc\",\"oid\"],\"oid\":[\"digest/oid\"],\"zeroize\":[\"digest/zeroize\"]}}", + "sha3_0.10.9": "{\"dependencies\":[{\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.2.2\"},{\"name\":\"keccak\",\"req\":\"^0.1.4\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.6.0\"}],\"features\":{\"asm\":[\"keccak/asm\"],\"default\":[\"std\"],\"oid\":[\"digest/oid\"],\"reset\":[],\"std\":[\"digest/std\"]}}", "sharded-slab_0.1.7": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"indexmap\",\"req\":\"^1\"},{\"name\":\"lazy_static\",\"req\":\"^1\"},{\"features\":[\"checkpoint\"],\"name\":\"loom\",\"optional\":true,\"req\":\"^0.5\",\"target\":\"cfg(loom)\"},{\"features\":[\"checkpoint\"],\"kind\":\"dev\",\"name\":\"loom\",\"req\":\"^0.5\",\"target\":\"cfg(loom)\"},{\"kind\":\"dev\",\"name\":\"memory-stats\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"slab\",\"req\":\"^0.4.2\"}],\"features\":{}}", "shared_library_0.1.9": "{\"dependencies\":[{\"name\":\"lazy_static\",\"req\":\"^1\"},{\"name\":\"libc\",\"req\":\"^0.2\"}],\"features\":{}}", "shell-words_1.1.1": "{\"dependencies\":[],\"features\":{\"default\":[\"std\"],\"std\":[]}}", @@ -1604,8 +1614,10 @@ "tester_0.9.1": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"name\":\"getopts\",\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"name\":\"num_cpus\",\"req\":\"^1.13.0\"},{\"name\":\"term\",\"req\":\"^0.7\"}],\"features\":{\"asm_black_box\":[],\"capture\":[]}}", "textwrap_0.11.0": "{\"dependencies\":[{\"features\":[\"embed_all\"],\"name\":\"hyphenation\",\"optional\":true,\"req\":\"^0.7.1\"},{\"kind\":\"dev\",\"name\":\"lipsum\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"rand_xorshift\",\"req\":\"^0.1\"},{\"name\":\"term_size\",\"optional\":true,\"req\":\"^0.3.0\"},{\"name\":\"unicode-width\",\"req\":\"^0.1.3\"},{\"kind\":\"dev\",\"name\":\"version-sync\",\"req\":\"^0.6\"}],\"features\":{}}", "textwrap_0.16.2": "{\"dependencies\":[{\"features\":[\"embed_en-us\"],\"name\":\"hyphenation\",\"optional\":true,\"req\":\"^0.8.4\"},{\"name\":\"smawk\",\"optional\":true,\"req\":\"^0.3.2\"},{\"name\":\"terminal_size\",\"optional\":true,\"req\":\"^0.4.0\"},{\"kind\":\"dev\",\"name\":\"termion\",\"req\":\"^4.0.2\",\"target\":\"cfg(unix)\"},{\"kind\":\"dev\",\"name\":\"unic-emoji-char\",\"req\":\"^0.9.0\"},{\"name\":\"unicode-linebreak\",\"optional\":true,\"req\":\"^0.1.5\"},{\"name\":\"unicode-width\",\"optional\":true,\"req\":\"^0.2.0\"},{\"kind\":\"dev\",\"name\":\"version-sync\",\"req\":\"^0.9.5\"}],\"features\":{\"default\":[\"unicode-linebreak\",\"unicode-width\",\"smawk\"]}}", + "thiserror-impl-no-std_2.0.2": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"name\":\"syn\",\"req\":\"^1.0.45\"}],\"features\":{\"std\":[]}}", "thiserror-impl_1.0.69": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.74\"},{\"name\":\"quote\",\"req\":\"^1.0.35\"},{\"name\":\"syn\",\"req\":\"^2.0.87\"}],\"features\":{}}", "thiserror-impl_2.0.18": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.74\"},{\"name\":\"quote\",\"req\":\"^1.0.35\"},{\"name\":\"syn\",\"req\":\"^2.0.87\"}],\"features\":{}}", + "thiserror-no-std_2.0.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"ref-cast\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"name\":\"thiserror-impl-no-std\",\"req\":\"=2.0.2\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.49\"}],\"features\":{\"std\":[\"thiserror-impl-no-std/std\"]}}", "thiserror_1.0.69": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.73\"},{\"kind\":\"dev\",\"name\":\"ref-cast\",\"req\":\"^1.0.18\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.13\"},{\"name\":\"thiserror-impl\",\"req\":\"=1.0.69\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.81\"}],\"features\":{}}", "thiserror_2.0.18": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.73\"},{\"kind\":\"dev\",\"name\":\"ref-cast\",\"req\":\"^1.0.18\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.13\"},{\"name\":\"thiserror-impl\",\"req\":\"=2.0.18\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.108\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "thread_local_1.1.9": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.1\"}],\"features\":{\"nightly\":[]}}", diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 52f6a67d6..952f1e5f2 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -196,6 +196,20 @@ dependencies = [ "cpufeatures 0.2.17", ] +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "age" version = "0.11.2" @@ -452,6 +466,9 @@ name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +dependencies = [ + "zeroize", +] [[package]] name = "ascii" @@ -1347,7 +1364,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" dependencies = [ - "hybrid-array", + "hybrid-array 0.4.12", ] [[package]] @@ -1767,6 +1784,24 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +[[package]] +name = "clatter" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fed49fa357a85c377c0f920e86100f5326111b09ad69f6de684e324e3ad8097" +dependencies = [ + "aes-gcm", + "arrayvec", + "displaydoc", + "getrandom 0.3.4", + "ml-kem", + "rand_core 0.6.4", + "sha2 0.10.9", + "thiserror-no-std", + "x25519-dalek", + "zeroize", +] + [[package]] name = "clipboard-win" version = "5.4.1" @@ -2838,6 +2873,7 @@ dependencies = [ "axum", "base64 0.22.1", "bytes", + "clatter", "codex-api", "codex-app-server-protocol", "codex-client", @@ -4790,7 +4826,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ - "hybrid-array", + "hybrid-array 0.4.12", ] [[package]] @@ -4870,6 +4906,15 @@ version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "ctutils" version = "0.4.2" @@ -6329,6 +6374,16 @@ dependencies = [ "wasip3", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gif" version = "0.14.1" @@ -7651,6 +7706,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2d35805454dc9f8662a98d6d61886ffe26bd465f5960e0e55345c70d5c0d2a9" +dependencies = [ + "typenum", +] + [[package]] name = "hybrid-array" version = "0.4.12" @@ -8517,6 +8581,25 @@ dependencies = [ "simple_asn1", ] +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + +[[package]] +name = "kem" +version = "0.3.0-pre.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8645470337db67b01a7f966decf7d0bafedbae74147d33e641c67a91df239f" +dependencies = [ + "rand_core 0.6.4", + "zeroize", +] + [[package]] name = "keyring" version = "3.6.3" @@ -9049,6 +9132,19 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ml-kem" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de49b3df74c35498c0232031bb7e85f9389f913e2796169c8ab47a53993a18f" +dependencies = [ + "hybrid-array 0.2.3", + "kem", + "rand_core 0.6.4", + "sha3", + "zeroize", +] + [[package]] name = "moka" version = "0.12.13" @@ -10159,6 +10255,18 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -12285,6 +12393,16 @@ dependencies = [ "digest 0.11.3", ] +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest 0.10.7", + "keccak", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -13261,6 +13379,26 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "thiserror-impl-no-std" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58e6318948b519ba6dc2b442a6d0b904ebfb8d411a3ad3e07843615a72249758" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "thiserror-no-std" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3ad459d94dd517257cc96add8a43190ee620011bb6e6cdc82dafd97dfafafea" +dependencies = [ + "thiserror-impl-no-std", +] + [[package]] name = "thread_local" version = "1.1.9" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index fcf8dbd9c..e5f5025e9 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -279,6 +279,14 @@ chardetng = "0.1.17" chrono = "0.4.43" clap = "4" clap_complete = "4" +clatter = { version = "2.2.0", default-features = false, features = [ + "alloc", + "getrandom", + "use-25519", + "use-aes-gcm", + "use-rust-crypto-ml-kem", + "use-sha", +] } color-eyre = "0.6.3" constant_time_eq = "0.3.1" crossbeam-channel = "0.5.15" diff --git a/codex-rs/core-api/src/lib.rs b/codex-rs/core-api/src/lib.rs index 9b2626543..df4e6ea8a 100644 --- a/codex-rs/core-api/src/lib.rs +++ b/codex-rs/core-api/src/lib.rs @@ -48,7 +48,12 @@ pub use codex_core::resolve_installation_id; pub use codex_core::skills::SkillsManager; pub use codex_core::thread_store_from_config; pub use codex_exec_server::EnvironmentManager; +pub use codex_exec_server::ExecServerError; pub use codex_exec_server::ExecServerRuntimePaths; +pub use codex_exec_server::NoiseChannelIdentity; +pub use codex_exec_server::NoiseChannelPublicKey; +pub use codex_exec_server::NoiseRendezvousConnectBundle; +pub use codex_exec_server::NoiseRendezvousConnectProvider; pub use codex_extension_api::LoadUserInstructionsFuture; pub use codex_extension_api::LoadedUserInstructions; pub use codex_extension_api::UserInstructions; diff --git a/codex-rs/exec-server/Cargo.toml b/codex-rs/exec-server/Cargo.toml index 687a1ee59..3c5408ba7 100644 --- a/codex-rs/exec-server/Cargo.toml +++ b/codex-rs/exec-server/Cargo.toml @@ -15,6 +15,7 @@ arc-swap = { workspace = true } axum = { workspace = true, features = ["http1", "tokio", "ws"] } base64 = { workspace = true } bytes = { workspace = true } +clatter = { workspace = true } codex-app-server-protocol = { workspace = true } codex-api = { workspace = true } codex-client = { workspace = true } diff --git a/codex-rs/exec-server/src/client.rs b/codex-rs/exec-server/src/client.rs index 978c502b7..5c9a5166f 100644 --- a/codex-rs/exec-server/src/client.rs +++ b/codex-rs/exec-server/src/client.rs @@ -231,6 +231,7 @@ impl LazyRemoteExecServerClient { if matches!( &self.transport_params, ExecServerTransportParams::WebSocketUrl { .. } + | ExecServerTransportParams::NoiseRendezvous { .. } ) => { ExecServerClient::connect_for_transport(self.transport_params.clone()).await? diff --git a/codex-rs/exec-server/src/client_api.rs b/codex-rs/exec-server/src/client_api.rs index 899863723..60dc638d4 100644 --- a/codex-rs/exec-server/src/client_api.rs +++ b/codex-rs/exec-server/src/client_api.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::path::PathBuf; +use std::sync::Arc; use std::time::Duration; use futures::future::BoxFuture; @@ -8,6 +9,8 @@ use crate::ExecServerError; use crate::HttpRequestParams; use crate::HttpRequestResponse; use crate::HttpResponseBodyStream; +use crate::NoiseChannelIdentity; +use crate::NoiseChannelPublicKey; pub(crate) const DEFAULT_REMOTE_EXEC_SERVER_CONNECT_TIMEOUT: Duration = Duration::from_secs(10); pub(crate) const DEFAULT_REMOTE_EXEC_SERVER_INITIALIZE_TIMEOUT: Duration = Duration::from_secs(10); @@ -30,6 +33,43 @@ pub struct RemoteExecServerConnectArgs { pub resume_session_id: Option, } +/// Registry-authorized material for one Noise rendezvous connection attempt. +/// +/// Treat this as an atomic, single-use bundle. The URL authorization, executor +/// registration, pinned executor key, and harness-key authorization describe one +/// physical connection attempt and must not be mixed with values from another +/// registry response. +pub struct NoiseRendezvousConnectBundle { + pub websocket_url: String, + pub environment_id: String, + pub executor_registration_id: String, + pub executor_public_key: NoiseChannelPublicKey, + pub harness_key_authorization: String, +} + +/// Connection arguments for an authenticated Noise rendezvous exec-server. +/// +/// `harness_identity` identifies the logical harness endpoint and may be reused +/// across reconnects. In contrast, callers must supply a fresh +/// [`NoiseRendezvousConnectBundle`] for each physical connection attempt. +pub struct NoiseRendezvousConnectArgs { + pub bundle: NoiseRendezvousConnectBundle, + pub harness_identity: NoiseChannelIdentity, + pub client_name: String, + pub connect_timeout: Duration, + pub initialize_timeout: Duration, + pub resume_session_id: Option, +} + +/// Supplies fresh registry-authorized material for Noise rendezvous connections. +pub trait NoiseRendezvousConnectProvider: Send + Sync { + /// Fetch a bundle authorizing this harness key for one physical connection. + fn connect_bundle( + &self, + harness_public_key: NoiseChannelPublicKey, + ) -> BoxFuture<'_, Result>; +} + /// Stdio connection arguments for a command-backed exec-server. #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct StdioExecServerConnectArgs { @@ -49,13 +89,17 @@ pub(crate) struct StdioExecServerCommand { } /// Parameters used to connect to a remote exec-server environment. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Clone)] pub(crate) enum ExecServerTransportParams { WebSocketUrl { websocket_url: String, connect_timeout: Duration, initialize_timeout: Duration, }, + NoiseRendezvous { + provider: Arc, + identity: NoiseChannelIdentity, + }, #[allow(dead_code)] StdioCommand { command: StdioExecServerCommand, @@ -63,6 +107,34 @@ pub(crate) enum ExecServerTransportParams { }, } +impl std::fmt::Debug for ExecServerTransportParams { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::WebSocketUrl { + websocket_url, + connect_timeout, + initialize_timeout, + } => f + .debug_struct("WebSocketUrl") + .field("websocket_url", websocket_url) + .field("connect_timeout", connect_timeout) + .field("initialize_timeout", initialize_timeout) + .finish(), + Self::NoiseRendezvous { .. } => { + f.debug_struct("NoiseRendezvous").finish_non_exhaustive() + } + Self::StdioCommand { + command, + initialize_timeout, + } => f + .debug_struct("StdioCommand") + .field("command", command) + .field("initialize_timeout", initialize_timeout) + .finish(), + } + } +} + impl ExecServerTransportParams { pub(crate) fn websocket_url(websocket_url: String) -> Self { Self::WebSocketUrl { diff --git a/codex-rs/exec-server/src/client_transport.rs b/codex-rs/exec-server/src/client_transport.rs index 4bdc09a80..c31a8fcdf 100644 --- a/codex-rs/exec-server/src/client_transport.rs +++ b/codex-rs/exec-server/src/client_transport.rs @@ -4,6 +4,7 @@ use tokio::io::BufReader; use tokio::process::Command; use tokio::time::timeout; use tokio_tungstenite::connect_async; +use tokio_tungstenite::connect_async_with_config; use tracing::debug; use tracing::warn; @@ -11,15 +12,25 @@ use codex_utils_rustls_provider::ensure_rustls_crypto_provider; use crate::ExecServerClient; use crate::ExecServerError; +use crate::client_api::DEFAULT_REMOTE_EXEC_SERVER_CONNECT_TIMEOUT; +use crate::client_api::DEFAULT_REMOTE_EXEC_SERVER_INITIALIZE_TIMEOUT; +use crate::client_api::NoiseRendezvousConnectArgs; +use crate::client_api::NoiseRendezvousConnectBundle; use crate::client_api::RemoteExecServerConnectArgs; use crate::client_api::StdioExecServerCommand; use crate::client_api::StdioExecServerConnectArgs; use crate::connection::JsonRpcConnection; +use crate::noise_relay::NoiseHarnessConnectionArgs; +use crate::noise_relay::noise_harness_connection_from_websocket; +use crate::noise_relay::noise_relay_websocket_config; use crate::relay::harness_connection_from_websocket; const ENVIRONMENT_CLIENT_NAME: &str = "codex-environment"; impl ExecServerClient { + /// Open the selected transport and run the common JSON-RPC initialization. + /// Noise connection details are fetched here so reconnects get a fresh URL + /// and authorization without replacing the harness identity. pub(crate) async fn connect_for_transport( transport_params: crate::client_api::ExecServerTransportParams, ) -> Result { @@ -38,6 +49,21 @@ impl ExecServerClient { }) .await } + crate::client_api::ExecServerTransportParams::NoiseRendezvous { + provider, + identity, + } => { + let bundle = provider.connect_bundle(identity.public_key()).await?; + Self::connect_noise_rendezvous(NoiseRendezvousConnectArgs { + bundle, + harness_identity: identity, + client_name: ENVIRONMENT_CLIENT_NAME.to_string(), + connect_timeout: DEFAULT_REMOTE_EXEC_SERVER_CONNECT_TIMEOUT, + initialize_timeout: DEFAULT_REMOTE_EXEC_SERVER_INITIALIZE_TIMEOUT, + resume_session_id: None, + }) + .await + } crate::client_api::ExecServerTransportParams::StdioCommand { command, initialize_timeout, @@ -79,6 +105,76 @@ impl ExecServerClient { Self::connect(connection, args.into()).await } + /// Connect to one exec-server through an authenticated rendezvous stream. + /// The executor key is pinned before JSON-RPC starts; the websocket carries + /// only ciphertext after that. + pub async fn connect_noise_rendezvous( + args: NoiseRendezvousConnectArgs, + ) -> Result { + ensure_rustls_crypto_provider(); + // Keep the registry-issued URL, key, and authorization together for this + // connection attempt. + let NoiseRendezvousConnectArgs { + bundle, + harness_identity, + client_name, + connect_timeout, + initialize_timeout, + resume_session_id, + } = args; + let NoiseRendezvousConnectBundle { + websocket_url, + environment_id, + executor_registration_id, + executor_public_key, + harness_key_authorization, + } = bundle; + let diagnostic_url = websocket_url + .split(['?', '#']) + .next() + .unwrap_or(websocket_url.as_str()) + .to_string(); + let (stream, _) = timeout( + connect_timeout, + connect_async_with_config( + websocket_url.as_str(), + Some(noise_relay_websocket_config()), + /*disable_nagle*/ false, + ), + ) + .await + .map_err(|_| ExecServerError::WebSocketConnectTimeout { + url: diagnostic_url.clone(), + timeout: connect_timeout, + })? + .map_err(|source| ExecServerError::WebSocketConnect { + url: diagnostic_url.clone(), + source, + })?; + + let connection_label = format!("Noise exec-server rendezvous websocket {diagnostic_url}"); + let connection = noise_harness_connection_from_websocket( + stream, + NoiseHarnessConnectionArgs { + connection_label, + environment_id, + executor_registration_id, + identity: harness_identity, + responder_public_key: executor_public_key, + harness_key_authorization, + }, + ); + Self::connect( + connection, + crate::client_api::ExecServerClientConnectOptions { + client_name, + initialize_timeout, + resume_session_id, + }, + ) + .await + } + pub(crate) async fn connect_stdio_command( args: StdioExecServerConnectArgs, ) -> Result { diff --git a/codex-rs/exec-server/src/connection.rs b/codex-rs/exec-server/src/connection.rs index b211c504a..2d2e5afe0 100644 --- a/codex-rs/exec-server/src/connection.rs +++ b/codex-rs/exec-server/src/connection.rs @@ -44,6 +44,7 @@ pub(crate) enum JsonRpcConnectionEvent { #[derive(Clone)] pub(crate) enum JsonRpcTransport { + // Plain means no child process; transport bytes may still be encrypted. Plain, Stdio { transport: StdioTransport }, } diff --git a/codex-rs/exec-server/src/environment.rs b/codex-rs/exec-server/src/environment.rs index d4cfef454..0c703ecef 100644 --- a/codex-rs/exec-server/src/environment.rs +++ b/codex-rs/exec-server/src/environment.rs @@ -9,6 +9,8 @@ use crate::ExecServerError; use crate::ExecServerRuntimePaths; use crate::ExecutorFileSystem; use crate::HttpClient; +use crate::NoiseChannelIdentity; +use crate::NoiseRendezvousConnectProvider; use crate::client::LazyRemoteExecServerClient; use crate::client::http_client::ReqwestHttpClient; use crate::client_api::ExecServerTransportParams; @@ -282,6 +284,37 @@ impl EnvironmentManager { .insert(environment_id, Arc::new(environment)); Ok(()) } + + /// Adds or replaces a named remote environment that connects through an + /// authenticated, end-to-end encrypted rendezvous stream. + /// + /// The provider is retained so every reconnect obtains fresh authorization. + /// This transport never falls back to the URL-only remote environment path. + pub fn upsert_noise_environment( + &self, + environment_id: String, + provider: Arc, + ) -> Result<(), ExecServerError> { + if environment_id.is_empty() { + return Err(ExecServerError::Protocol( + "environment id cannot be empty".to_string(), + )); + } + let identity = NoiseChannelIdentity::generate().map_err(|error| { + ExecServerError::Protocol(format!( + "failed to generate Noise harness identity: {error}" + )) + })?; + let environment = Environment::remote_with_transport( + ExecServerTransportParams::NoiseRendezvous { provider, identity }, + self.local_runtime_paths.clone(), + ); + self.environments + .write() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .insert(environment_id, Arc::new(environment)); + Ok(()) + } } /// Concrete execution/filesystem environment selected for a session. @@ -420,6 +453,7 @@ impl Environment { websocket_url: exec_server_url, .. } => Some(exec_server_url.clone()), + ExecServerTransportParams::NoiseRendezvous { .. } => None, ExecServerTransportParams::StdioCommand { .. } => None, }; let client = LazyRemoteExecServerClient::new(remote_transport.clone()); diff --git a/codex-rs/exec-server/src/environment_toml.rs b/codex-rs/exec-server/src/environment_toml.rs index 87d5bea8b..bfc25873b 100644 --- a/codex-rs/exec-server/src/environment_toml.rs +++ b/codex-rs/exec-server/src/environment_toml.rs @@ -48,7 +48,7 @@ struct EnvironmentToml { initialize_timeout_sec: Option, } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug)] struct TomlEnvironmentProvider { default: EnvironmentDefault, include_local: bool, @@ -580,18 +580,26 @@ mod tests { ) .expect("provider"); + let ExecServerTransportParams::StdioCommand { + command, + initialize_timeout, + } = &provider.environments[0].1 + else { + panic!("expected stdio transport"); + }; assert_eq!( - provider.environments[0].1, - ExecServerTransportParams::StdioCommand { - command: StdioExecServerCommand { - program: "ssh".to_string(), - args: Vec::new(), - env: HashMap::new(), - cwd: Some(config_dir.path().join("workspace")), - }, - initialize_timeout: DEFAULT_REMOTE_EXEC_SERVER_INITIALIZE_TIMEOUT, + command, + &StdioExecServerCommand { + program: "ssh".to_string(), + args: Vec::new(), + env: HashMap::new(), + cwd: Some(config_dir.path().join("workspace")), } ); + assert_eq!( + *initialize_timeout, + DEFAULT_REMOTE_EXEC_SERVER_INITIALIZE_TIMEOUT + ); } #[test] @@ -617,26 +625,35 @@ mod tests { }) .expect("provider"); + let ExecServerTransportParams::WebSocketUrl { + websocket_url, + connect_timeout, + initialize_timeout, + } = &provider.environments[0].1 + else { + panic!("expected websocket transport"); + }; + assert_eq!(websocket_url, "ws://127.0.0.1:8765"); + assert_eq!(*connect_timeout, Duration::from_secs(12)); + assert_eq!(*initialize_timeout, Duration::from_secs(34)); + + let ExecServerTransportParams::StdioCommand { + command, + initialize_timeout, + } = &provider.environments[1].1 + else { + panic!("expected stdio transport"); + }; assert_eq!( - provider.environments[0].1, - ExecServerTransportParams::WebSocketUrl { - websocket_url: "ws://127.0.0.1:8765".to_string(), - connect_timeout: Duration::from_secs(12), - initialize_timeout: Duration::from_secs(34), - } - ); - assert_eq!( - provider.environments[1].1, - ExecServerTransportParams::StdioCommand { - command: StdioExecServerCommand { - program: "ssh".to_string(), - args: Vec::new(), - env: HashMap::new(), - cwd: None, - }, - initialize_timeout: Duration::from_secs(56), + command, + &StdioExecServerCommand { + program: "ssh".to_string(), + args: Vec::new(), + env: HashMap::new(), + cwd: None, } ); + assert_eq!(*initialize_timeout, Duration::from_secs(56)); } #[test] diff --git a/codex-rs/exec-server/src/lib.rs b/codex-rs/exec-server/src/lib.rs index d6a9aacb5..8c7aff8a5 100644 --- a/codex-rs/exec-server/src/lib.rs +++ b/codex-rs/exec-server/src/lib.rs @@ -10,6 +10,8 @@ mod fs_helper_main; mod fs_sandbox; mod local_file_system; mod local_process; +mod noise_channel; +mod noise_relay; mod process; mod process_id; mod protocol; @@ -29,6 +31,9 @@ pub use client::http_client::HttpResponseBodyStream; pub use client::http_client::ReqwestHttpClient; pub use client_api::ExecServerClientConnectOptions; pub use client_api::HttpClient; +pub use client_api::NoiseRendezvousConnectArgs; +pub use client_api::NoiseRendezvousConnectBundle; +pub use client_api::NoiseRendezvousConnectProvider; pub use client_api::RemoteExecServerConnectArgs; pub use codex_file_system::CopyOptions; pub use codex_file_system::CreateDirectoryOptions; @@ -51,6 +56,9 @@ pub use fs_helper::CODEX_FS_HELPER_ARG1; pub use fs_helper_main::main as run_fs_helper_main; pub use local_file_system::LOCAL_FS; pub use local_file_system::LocalFileSystem; +pub use noise_channel::NoiseChannelError; +pub use noise_channel::NoiseChannelIdentity; +pub use noise_channel::NoiseChannelPublicKey; pub use process::ExecBackend; pub use process::ExecBackendFuture; pub use process::ExecProcess; diff --git a/codex-rs/exec-server/src/noise_channel.rs b/codex-rs/exec-server/src/noise_channel.rs new file mode 100644 index 000000000..42cc2e6b3 --- /dev/null +++ b/codex-rs/exec-server/src/noise_channel.rs @@ -0,0 +1,261 @@ +//! Noise channel used by the remote exec-server relay. +//! +//! The harness initiates hybrid IK and pins the exec-server static key returned +//! by the registry. The first handshake message lets the exec-server authenticate +//! the harness static key; the exec-server then asks the registry whether that +//! key is authorized before completing the handshake. +//! +//! "Hybrid" means the session keys include both X25519 and ML-KEM-768 key +//! agreement. Once the two-message handshake finishes, AES-GCM protects the +//! ordered transport records carrying JSON-RPC. + +use base64::Engine; +use base64::engine::general_purpose::STANDARD; +use clatter::HybridHandshake; +use clatter::HybridHandshakeParams; +use clatter::KeyPair; +use clatter::bytearray::ByteArray; +use clatter::constants::MAX_MESSAGE_LEN; +use clatter::crypto::cipher::AesGcm; +use clatter::crypto::dh::X25519; +use clatter::crypto::hash::Sha256; +use clatter::crypto::kem::rust_crypto_ml_kem::MlKem768; +use clatter::handshakepattern::noise_hybrid_ik; +use clatter::traits::Cipher; +use clatter::traits::Dh; +use clatter::traits::Handshaker; +use clatter::traits::Kem; +use clatter::transportstate::TransportState; +use serde::Deserialize; +use serde::Serialize; + +/// Identifies the handshake pattern and algorithms used by this channel. +pub(crate) const NOISE_CHANNEL_SUITE: &str = "Noise_hybridIK_X25519+MLKEM768_AESGCM_SHA256"; + +const PROLOGUE_DOMAIN: &[u8] = b"codex-exec-server-relay-noise/v1"; + +type Handshake = HybridHandshake; +type Transport = TransportState; +type DhKeyPair = KeyPair<::PubKey, ::PrivateKey>; +type MlKem768PublicKey = ::PubKey; +type KemKeyPair = KeyPair<::PubKey, ::SecretKey>; + +/// Public key material for the exec-server Noise suite. +/// The suite tag prevents keys for another protocol from being accepted just +/// because their components have the expected lengths. +#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct NoiseChannelPublicKey { + suite: String, + x25519_public_key: String, + mlkem768_public_key: String, +} + +impl std::fmt::Debug for NoiseChannelPublicKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("NoiseChannelPublicKey") + .field("suite", &self.suite) + .field("x25519_public_key", &"") + .field("mlkem768_public_key", &"") + .finish() + } +} + +impl NoiseChannelPublicKey { + /// Decode registry-provided key material before passing it to Clatter. + fn decode(&self) -> Result<(::PubKey, MlKem768PublicKey), NoiseChannelError> { + if self.suite != NOISE_CHANNEL_SUITE { + return Err(NoiseChannelError::InvalidPublicKey( + "unsupported Noise channel suite", + )); + } + let dh = STANDARD + .decode(&self.x25519_public_key) + .map_err(|_| NoiseChannelError::InvalidPublicKey("invalid X25519 public key"))?; + let dh: ::PubKey = dh + .try_into() + .map_err(|_| NoiseChannelError::InvalidPublicKey("invalid X25519 public key length"))?; + let kem = STANDARD + .decode(&self.mlkem768_public_key) + .map_err(|_| NoiseChannelError::InvalidPublicKey("invalid ML-KEM-768 public key"))?; + if kem.len() != MlKem768PublicKey::LENGTH { + return Err(NoiseChannelError::InvalidPublicKey( + "invalid ML-KEM-768 public key length", + )); + } + + Ok((dh, MlKem768PublicKey::from_slice(kem.as_slice()))) + } +} + +/// Static Noise identity kept for the lifetime of an executor or harness process. +#[derive(Clone)] +pub struct NoiseChannelIdentity { + dh: DhKeyPair, + kem: KemKeyPair, +} + +impl NoiseChannelIdentity { + pub fn generate() -> Result { + let dh = X25519::genkey() + .map_err(|error| NoiseChannelError::KeyGeneration(error.to_string()))?; + let kem = MlKem768::genkey() + .map_err(|error| NoiseChannelError::KeyGeneration(error.to_string()))?; + Ok(Self { dh, kem }) + } + + pub fn public_key(&self) -> NoiseChannelPublicKey { + NoiseChannelPublicKey { + suite: NOISE_CHANNEL_SUITE.to_string(), + x25519_public_key: STANDARD.encode(self.dh.public), + mlkem768_public_key: STANDARD.encode(self.kem.public.as_slice()), + } + } +} + +/// Harness-side state between the two hybrid-IK messages. +/// Consuming it in [`Self::finish`] keeps a handshake tied to one relay stream. +pub(crate) struct InitiatorHandshake { + handshake: Handshake, +} + +impl InitiatorHandshake { + /// Start hybrid IK and pin the expected executor key. + /// `payload` carries the short-lived registry authorization inside the first + /// encrypted handshake message. + pub(crate) fn start( + identity: &NoiseChannelIdentity, + responder_public_key: &NoiseChannelPublicKey, + prologue: &[u8], + payload: &[u8], + ) -> Result<(Self, Vec), NoiseChannelError> { + let (responder_dh, responder_kem) = responder_public_key.decode()?; + + // Both executor key components are pinned before any JSON-RPC is sent. + let params = HybridHandshakeParams::new(noise_hybrid_ik(), true) + .with_prologue(prologue) + .with_s(identity.dh.clone()) + .with_s_kem(identity.kem.clone()) + .with_rs(responder_dh) + .with_rs_kem(responder_kem); + let mut handshake = Handshake::new(params)?; + let overhead = handshake.get_next_message_overhead()?; + if payload.len() > MAX_MESSAGE_LEN - overhead { + return Err(NoiseChannelError::InvalidMessage( + "handshake payload is too large", + )); + } + let mut output = [0u8; MAX_MESSAGE_LEN]; + let output_len = handshake.write_message(payload, &mut output)?; + Ok((Self { handshake }, output[..output_len].to_vec())) + } + + /// Consume the executor response and enter transport mode. + /// The v1 response does not carry an application payload. + pub(crate) fn finish(mut self, response: &[u8]) -> Result { + ensure_noise_frame_len(response.len(), "handshake response is too large")?; + let mut payload = [0u8; MAX_MESSAGE_LEN]; + let payload_len = self.handshake.read_message(response, &mut payload)?; + if payload_len != 0 { + return Err(NoiseChannelError::InvalidMessage( + "handshake response payload must be empty", + )); + } + Ok(NoiseTransport { + transport: self.handshake.finalize()?, + }) + } +} + +/// Established channel with independent implicit send and receive nonces. +/// Relay records must be ordered before decryption, and a logical record must +/// not be encrypted again for retry. +pub(crate) struct NoiseTransport { + transport: Transport, +} + +impl NoiseTransport { + /// Encrypt the next transport record. + pub(crate) fn encrypt(&mut self, plaintext: &[u8]) -> Result, NoiseChannelError> { + let frame_len = plaintext.len().checked_add(AesGcm::tag_len()).ok_or( + NoiseChannelError::InvalidMessage("transport plaintext is too large"), + )?; + ensure_noise_frame_len(frame_len, "transport plaintext is too large")?; + Ok(self.transport.send_vec(plaintext)?) + } + + /// Decrypt the next ordered transport record. + pub(crate) fn decrypt(&mut self, ciphertext: &[u8]) -> Result, NoiseChannelError> { + if ciphertext.len() < AesGcm::tag_len() { + return Err(NoiseChannelError::InvalidMessage( + "transport ciphertext is too short", + )); + } + ensure_noise_frame_len(ciphertext.len(), "transport ciphertext is too large")?; + Ok(self.transport.receive_vec(ciphertext)?) + } +} + +/// Bind the handshake to one environment registration and relay stream. +/// Both peers include these values in the Noise transcript before processing +/// the first handshake message. +pub(crate) fn noise_channel_prologue( + environment_id: &str, + executor_registration_id: &str, + stream_id: &str, +) -> Vec { + let mut prologue = Vec::new(); + append_prologue_part(&mut prologue, PROLOGUE_DOMAIN); + append_prologue_part(&mut prologue, environment_id.as_bytes()); + append_prologue_part(&mut prologue, executor_registration_id.as_bytes()); + append_prologue_part(&mut prologue, stream_id.as_bytes()); + prologue +} + +fn append_prologue_part(prologue: &mut Vec, part: &[u8]) { + // Length prefixes make component boundaries unambiguous. Raw concatenation + // would allow different identifier tuples to produce the same prologue. + let len = part.len() as u64; + prologue.extend_from_slice(&len.to_be_bytes()); + prologue.extend_from_slice(part); +} + +fn ensure_noise_frame_len( + frame_len: usize, + message: &'static str, +) -> Result<(), NoiseChannelError> { + if frame_len > MAX_MESSAGE_LEN { + return Err(NoiseChannelError::InvalidMessage(message)); + } + Ok(()) +} + +#[derive(Debug, thiserror::Error)] +pub enum NoiseChannelError { + #[error("Noise channel key generation failed: {0}")] + KeyGeneration(String), + #[error("invalid Noise channel public key: {0}")] + InvalidPublicKey(&'static str), + #[error("invalid Noise channel message: {0}")] + InvalidMessage(&'static str), + #[error("Noise channel handshake failed: {0}")] + Handshake(String), + #[error("Noise channel transport failed: {0}")] + Transport(String), +} + +impl From for NoiseChannelError { + fn from(error: clatter::error::HandshakeError) -> Self { + Self::Handshake(error.to_string()) + } +} + +impl From for NoiseChannelError { + fn from(error: clatter::error::TransportError) -> Self { + Self::Transport(error.to_string()) + } +} + +#[cfg(test)] +#[path = "noise_channel_tests.rs"] +mod tests; diff --git a/codex-rs/exec-server/src/noise_channel_tests.rs b/codex-rs/exec-server/src/noise_channel_tests.rs new file mode 100644 index 000000000..1638eef89 --- /dev/null +++ b/codex-rs/exec-server/src/noise_channel_tests.rs @@ -0,0 +1,51 @@ +use pretty_assertions::assert_eq; + +use super::InitiatorHandshake; +use super::MAX_MESSAGE_LEN; +use super::NOISE_CHANNEL_SUITE; +use super::NoiseChannelError; +use super::NoiseChannelIdentity; +use super::NoiseChannelPublicKey; + +#[test] +fn public_key_validation_rejects_unknown_suite() { + let key = NoiseChannelIdentity::generate() + .expect("generate identity") + .public_key(); + let json = serde_json::to_value(key).expect("serialize key"); + let mut object = json.as_object().expect("key object").clone(); + object.insert("suite".to_string(), serde_json::json!("unknown")); + let key: NoiseChannelPublicKey = + serde_json::from_value(serde_json::Value::Object(object)).expect("deserialize key"); + + let initiator = NoiseChannelIdentity::generate().expect("generate initiator identity"); + assert!(InitiatorHandshake::start(&initiator, &key, b"prologue", b"").is_err()); +} + +#[test] +fn public_key_serializes_with_expected_suite() { + let key = NoiseChannelIdentity::generate() + .expect("generate identity") + .public_key(); + + let json = serde_json::to_value(key).expect("serialize key"); + + assert_eq!(json["suite"], NOISE_CHANNEL_SUITE); +} + +#[test] +fn initiator_rejects_oversized_handshake_payload() { + let initiator = NoiseChannelIdentity::generate().expect("generate initiator identity"); + let responder = NoiseChannelIdentity::generate().expect("generate responder identity"); + let payload = vec![0; MAX_MESSAGE_LEN]; + + let result = + InitiatorHandshake::start(&initiator, &responder.public_key(), b"prologue", &payload); + + assert!(matches!( + result, + Err(NoiseChannelError::InvalidMessage( + "handshake payload is too large" + )) + )); +} diff --git a/codex-rs/exec-server/src/noise_relay/harness.rs b/codex-rs/exec-server/src/noise_relay/harness.rs new file mode 100644 index 000000000..a857ebfbe --- /dev/null +++ b/codex-rs/exec-server/src/noise_relay/harness.rs @@ -0,0 +1,427 @@ +//! Harness side of the Noise relay. +//! +//! The rendezvous service routes frames by `stream_id`, but does not authenticate +//! the executor or see JSON-RPC plaintext. We claim a stream, complete hybrid IK +//! against the registry-provided executor key, and then expose the result as a +//! normal `JsonRpcConnection`. Outbound JSON-RPC is framed and split into Noise +//! records; inbound records are reordered before decryption and reassembly. + +use futures::Sink; +use futures::SinkExt; +use futures::Stream; +use futures::StreamExt; +use tokio::sync::mpsc; +use tokio::sync::watch; +use tokio_tungstenite::tungstenite::Message; +use tracing::debug; +use tracing::warn; +use uuid::Uuid; + +use crate::ExecServerError; +use crate::connection::CHANNEL_CAPACITY; +use crate::connection::JsonRpcConnection; +use crate::connection::JsonRpcConnectionEvent; +use crate::connection::JsonRpcTransport; +use crate::noise_channel::InitiatorHandshake; +use crate::noise_channel::NoiseChannelIdentity; +use crate::noise_channel::NoiseChannelPublicKey; +use crate::noise_channel::NoiseTransport; +use crate::noise_channel::noise_channel_prologue; +use crate::noise_relay::message_framing::JsonRpcMessageDecoder; +use crate::noise_relay::message_framing::NOISE_RECORD_PLAINTEXT_LEN; +use crate::noise_relay::message_framing::frame_jsonrpc_message; +use crate::noise_relay::ordered_ciphertext::OrderedCiphertextFrames; +use crate::noise_relay::take_next_sequence; +use crate::relay::RelayFrameBodyKind; +use crate::relay::decode_relay_message_frame; +use crate::relay::encode_relay_message_frame; +use crate::relay_proto::RelayData; +use crate::relay_proto::RelayMessageFrame; + +/// Values that bind one harness websocket to the intended executor registration. +/// +/// These fields all come from the same registry response. Keeping them together +/// makes that relationship visible at the call site and avoids mixing up the +/// several string and key arguments used to start the handshake. +pub(crate) struct NoiseHarnessConnectionArgs { + pub(crate) connection_label: String, + pub(crate) environment_id: String, + pub(crate) executor_registration_id: String, + pub(crate) identity: NoiseChannelIdentity, + pub(crate) responder_public_key: NoiseChannelPublicKey, + pub(crate) harness_key_authorization: String, +} + +/// Adapt one harness rendezvous websocket into an authenticated JSON-RPC connection. +/// +/// The returned connection is not usable until the background task completes +/// hybrid IK against the registry-pinned exec-server key. Rendezvous can see +/// stream metadata and ciphertext, but never JSON-RPC plaintext or either +/// endpoint's private key. Failures close the connection rather than falling +/// back to plaintext. +pub(crate) fn noise_harness_connection_from_websocket( + stream: T, + args: NoiseHarnessConnectionArgs, +) -> JsonRpcConnection +where + T: Sink + Stream> + Unpin + Send + 'static, + E: std::fmt::Display + Send + 'static, +{ + let NoiseHarnessConnectionArgs { + connection_label, + environment_id, + executor_registration_id, + identity, + responder_public_key, + harness_key_authorization, + } = args; + let stream_id = Uuid::new_v4().to_string(); + let (outgoing_tx, mut outgoing_rx) = mpsc::channel(CHANNEL_CAPACITY); + let (incoming_tx, incoming_rx) = mpsc::channel(CHANNEL_CAPACITY); + let (disconnected_tx, disconnected_rx) = watch::channel(false); + + let websocket_task = tokio::spawn(async move { + let mut websocket = stream; + + // Bind the Noise transcript to the exact environment registration and + // virtual relay stream before emitting any handshake bytes. A captured + // handshake cannot be spliced onto a different routed connection. + let prologue = + noise_channel_prologue(&environment_id, &executor_registration_id, &stream_id); + let (initiator_handshake, request) = match InitiatorHandshake::start( + &identity, + &responder_public_key, + &prologue, + harness_key_authorization.as_bytes(), + ) { + Ok(handshake) => handshake, + Err(error) => { + send_disconnected( + &incoming_tx, + &disconnected_tx, + format!("failed to start Noise relay handshake: {error}"), + ) + .await; + return; + } + }; + + // Resume claims the stream ID at rendezvous; Handshake carries the + // opaque first IK message. No JSON-RPC data is sent before the + // responder proves possession of the pinned static key. + let resume = RelayMessageFrame::resume(stream_id.clone()); + let handshake = RelayMessageFrame::handshake(stream_id.clone(), request); + if websocket + .send(Message::Binary(encode_relay_message_frame(&resume).into())) + .await + .is_err() + || websocket + .send(Message::Binary( + encode_relay_message_frame(&handshake).into(), + )) + .await + .is_err() + { + let _ = disconnected_tx.send(true); + return; + } + + // During the handshake, ignore unrelated routed streams and control + // frames, but reject data on our stream. Accepting early data would + // create a plaintext or unauthenticated application path. + let mut transport = loop { + let Some(incoming_message) = websocket.next().await else { + send_disconnected( + &incoming_tx, + &disconnected_tx, + "Noise relay websocket ended during handshake".to_string(), + ) + .await; + return; + }; + let message = match incoming_message { + Ok(Message::Binary(payload)) => payload, + Ok(Message::Close(_)) => { + send_disconnected( + &incoming_tx, + &disconnected_tx, + "Noise relay websocket received close frame during handshake".to_string(), + ) + .await; + return; + } + Ok(Message::Ping(_) | Message::Pong(_) | Message::Frame(_)) => continue, + Ok(Message::Text(_)) => { + send_disconnected( + &incoming_tx, + &disconnected_tx, + "Noise relay transport expects binary protobuf frames".to_string(), + ) + .await; + return; + } + Err(error) => { + send_disconnected( + &incoming_tx, + &disconnected_tx, + format!( + "failed to read Noise relay websocket from {connection_label}: {error}" + ), + ) + .await; + return; + } + }; + let frame = match decode_relay_message_frame(message.as_ref()) { + Ok(frame) => frame, + Err(error) => { + send_disconnected( + &incoming_tx, + &disconnected_tx, + format!("failed to parse Noise relay frame: {error}"), + ) + .await; + return; + } + }; + if frame.stream_id != stream_id { + debug!("Noise relay ignored frame for unrelated stream during handshake"); + continue; + } + match frame.validate() { + Ok(RelayFrameBodyKind::Handshake) => { + let response = match frame.into_handshake_payload() { + Ok(response) => response, + Err(error) => { + send_disconnected( + &incoming_tx, + &disconnected_tx, + format!("invalid Noise relay handshake response: {error}"), + ) + .await; + return; + } + }; + match initiator_handshake.finish(&response) { + Ok(transport) => break transport, + Err(error) => { + send_disconnected( + &incoming_tx, + &disconnected_tx, + format!("Noise relay handshake failed: {error}"), + ) + .await; + return; + } + } + } + Ok(RelayFrameBodyKind::Reset) => { + send_disconnected( + &incoming_tx, + &disconnected_tx, + frame + .into_reset_reason() + .unwrap_or_else(|| "Noise relay reset during handshake".to_string()), + ) + .await; + return; + } + Ok( + RelayFrameBodyKind::Ack + | RelayFrameBodyKind::Resume + | RelayFrameBodyKind::Heartbeat, + ) => {} + Ok(RelayFrameBodyKind::Data) | Err(_) => { + send_disconnected( + &incoming_tx, + &disconnected_tx, + "Noise relay received data before handshake completion".to_string(), + ) + .await; + return; + } + } + }; + + // After the handshake, each relay sequence maps to exactly one Noise + // transport record. Outbound records are encrypted once; inbound + // records are reordered and deduplicated before decryption. + let mut next_outbound_seq = 0u32; + let mut inbound_ciphertexts = OrderedCiphertextFrames::default(); + let mut inbound_decoder = JsonRpcMessageDecoder::default(); + 'relay: loop { + tokio::select! { + maybe_message = outgoing_rx.recv() => { + let Some(message) = maybe_message else { + break; + }; + let framed = match frame_jsonrpc_message(&message) { + Ok(framed) => framed, + Err(error) => { + warn!("failed to frame JSON-RPC payload for Noise relay: {error}"); + break; + } + }; + for plaintext_record in framed.chunks(NOISE_RECORD_PLAINTEXT_LEN) { + let seq = match take_next_sequence(&mut next_outbound_seq) { + Ok(seq) => seq, + Err(error) => { + warn!("Noise relay sequence exhausted: {error}"); + break 'relay; + } + }; + let ciphertext = match transport.encrypt(plaintext_record) { + Ok(ciphertext) => ciphertext, + Err(error) => { + warn!("failed to encrypt JSON-RPC payload for Noise relay: {error}"); + break 'relay; + } + }; + let frame = RelayMessageFrame::data(stream_id.clone(), seq, ciphertext); + if let Err(error) = websocket + .send(Message::Binary(encode_relay_message_frame(&frame).into())) + .await + { + warn!("failed to write Noise relay websocket: {error}"); + break 'relay; + } + } + } + incoming_message = websocket.next() => { + let Some(incoming_message) = incoming_message else { + break; + }; + match incoming_message { + Ok(Message::Binary(payload)) => { + let frame = match decode_relay_message_frame(payload.as_ref()) { + Ok(frame) => frame, + Err(error) => { + send_malformed(&incoming_tx, error.to_string()).await; + break; + } + }; + if frame.stream_id != stream_id { + continue; + } + match frame.validate() { + Ok(RelayFrameBodyKind::Data) => { + let data = match frame.into_data() { + Ok(data) => data, + Err(error) => { + send_malformed(&incoming_tx, error.to_string()).await; + break; + } + }; + if let Err(error) = receive_data( + &mut inbound_ciphertexts, + &mut transport, + &mut inbound_decoder, + data, + &incoming_tx, + ) + .await + { + send_malformed(&incoming_tx, error.to_string()).await; + break; + } + } + Ok(RelayFrameBodyKind::Reset) => { + let reason = frame.into_reset_reason(); + let _ = incoming_tx + .send(JsonRpcConnectionEvent::Disconnected { reason }) + .await; + break; + } + Ok( + RelayFrameBodyKind::Ack + | RelayFrameBodyKind::Resume + | RelayFrameBodyKind::Heartbeat, + ) => {} + Ok(RelayFrameBodyKind::Handshake) | Err(_) => { + send_malformed( + &incoming_tx, + "Noise relay received invalid post-handshake frame".to_string(), + ) + .await; + break; + } + } + } + Ok(Message::Close(_)) => break, + Ok(Message::Ping(_) | Message::Pong(_) | Message::Frame(_)) => {} + Ok(Message::Text(_)) => { + send_malformed( + &incoming_tx, + "Noise relay transport expects binary protobuf frames".to_string(), + ) + .await; + break; + } + Err(error) => { + debug!("Noise relay websocket read failed: {error}"); + break; + } + } + } + } + } + let _ = disconnected_tx.send(true); + }); + + JsonRpcConnection { + outgoing_tx, + incoming_rx, + disconnected_rx, + task_handles: vec![websocket_task], + transport: JsonRpcTransport::Plain, + } +} + +/// Order and decrypt one relay frame, then emit any complete JSON-RPC messages. +/// Relay records and JSON-RPC messages do not share boundaries, so reassembly +/// happens after decryption. +async fn receive_data( + inbound_ciphertexts: &mut OrderedCiphertextFrames, + transport: &mut NoiseTransport, + decoder: &mut JsonRpcMessageDecoder, + data: RelayData, + incoming_tx: &mpsc::Sender, +) -> Result<(), ExecServerError> { + // Ordering must happen before decryption because Noise transport nonces are + // implicit. A future or duplicate ciphertext passed directly to Clatter + // would desynchronize the channel. + for ciphertext in inbound_ciphertexts.push(data.seq, data.payload)? { + let plaintext = transport.decrypt(&ciphertext).map_err(|error| { + ExecServerError::Protocol(format!("Noise relay decryption failed: {error}")) + })?; + + // The authenticated byte stream can carry partial or multiple JSON-RPC + // messages; emit only complete, successfully parsed messages. + for message in decoder.push(&plaintext)? { + incoming_tx + .send(JsonRpcConnectionEvent::Message(message)) + .await + .map_err(|_| ExecServerError::Closed)?; + } + } + Ok(()) +} + +async fn send_malformed(incoming_tx: &mpsc::Sender, reason: String) { + let _ = incoming_tx + .send(JsonRpcConnectionEvent::MalformedMessage { reason }) + .await; +} + +async fn send_disconnected( + incoming_tx: &mpsc::Sender, + disconnected_tx: &watch::Sender, + reason: String, +) { + let _ = disconnected_tx.send(true); + let _ = incoming_tx + .send(JsonRpcConnectionEvent::Disconnected { + reason: Some(reason), + }) + .await; +} diff --git a/codex-rs/exec-server/src/noise_relay/message_framing.rs b/codex-rs/exec-server/src/noise_relay/message_framing.rs new file mode 100644 index 000000000..24ee48173 --- /dev/null +++ b/codex-rs/exec-server/src/noise_relay/message_framing.rs @@ -0,0 +1,85 @@ +use codex_app_server_protocol::JSONRPCMessage; + +use crate::ExecServerError; + +const LENGTH_PREFIX_BYTES: usize = size_of::(); +const MAX_NOISE_JSONRPC_MESSAGE_LEN: usize = 64 * 1024 * 1024; +pub(crate) const NOISE_RECORD_PLAINTEXT_LEN: usize = 60 * 1024; + +/// Serialize one JSON-RPC message into the encrypted record byte stream. +/// +/// Clatter limits an individual Noise message to 65,535 bytes, while valid +/// exec-server responses can be much larger. A four-byte authenticated length +/// prefix lets the caller split this byte stream into bounded Noise records and +/// lets the receiver reconstruct exact JSON-RPC message boundaries. +pub(crate) fn frame_jsonrpc_message(message: &JSONRPCMessage) -> Result, ExecServerError> { + let mut framed = vec![0; LENGTH_PREFIX_BYTES]; + serde_json::to_writer(&mut framed, message)?; + let message_len = framed.len() - LENGTH_PREFIX_BYTES; + if message_len > MAX_NOISE_JSONRPC_MESSAGE_LEN { + return Err(ExecServerError::Protocol( + "Noise relay JSON-RPC message exceeds maximum length".to_string(), + )); + } + framed[..LENGTH_PREFIX_BYTES].copy_from_slice(&(message_len as u32).to_be_bytes()); + Ok(framed) +} + +/// Incrementally reconstructs authenticated JSON-RPC messages from Noise records. +/// +/// The length prefix is encrypted along with the message. It is still bounded +/// here so a bad authenticated peer cannot grow the reassembly buffer forever. +#[derive(Default)] +pub(crate) struct JsonRpcMessageDecoder { + buffered: Vec, +} + +impl JsonRpcMessageDecoder { + /// Append one decrypted record and return all complete framed messages. + pub(crate) fn push( + &mut self, + plaintext_record: &[u8], + ) -> Result, ExecServerError> { + if plaintext_record.len() > NOISE_RECORD_PLAINTEXT_LEN { + return Err(ExecServerError::Protocol( + "Noise relay plaintext record exceeds maximum length".to_string(), + )); + } + self.buffered.extend_from_slice(plaintext_record); + + // One record can finish multiple messages, and one message can span + // multiple records. Parse only after the authenticated length prefix + // and the full declared payload are present. + let mut messages = Vec::new(); + while let Some(prefix) = self.buffered.get(..LENGTH_PREFIX_BYTES) { + let message_len = + u32::from_be_bytes([prefix[0], prefix[1], prefix[2], prefix[3]]) as usize; + // Reject the authenticated length before waiting for its payload. + if message_len == 0 || message_len > MAX_NOISE_JSONRPC_MESSAGE_LEN { + return Err(ExecServerError::Protocol( + "Noise relay JSON-RPC message has invalid length".to_string(), + )); + } + let framed_len = LENGTH_PREFIX_BYTES + message_len; + if self.buffered.len() < framed_len { + break; + } + messages.push(serde_json::from_slice( + &self.buffered[LENGTH_PREFIX_BYTES..framed_len], + )?); + self.buffered.drain(..framed_len); + } + + // Even before a message is complete, keep reassembly memory bounded. + if self.buffered.len() > LENGTH_PREFIX_BYTES + MAX_NOISE_JSONRPC_MESSAGE_LEN { + return Err(ExecServerError::Protocol( + "Noise relay JSON-RPC reassembly buffer exceeds maximum length".to_string(), + )); + } + Ok(messages) + } +} + +#[cfg(test)] +#[path = "message_framing_tests.rs"] +mod tests; diff --git a/codex-rs/exec-server/src/noise_relay/message_framing_tests.rs b/codex-rs/exec-server/src/noise_relay/message_framing_tests.rs new file mode 100644 index 000000000..c3df14bf5 --- /dev/null +++ b/codex-rs/exec-server/src/noise_relay/message_framing_tests.rs @@ -0,0 +1,52 @@ +use codex_app_server_protocol::JSONRPCMessage; +use codex_app_server_protocol::JSONRPCNotification; +use pretty_assertions::assert_eq; + +use super::JsonRpcMessageDecoder; +use super::MAX_NOISE_JSONRPC_MESSAGE_LEN; +use super::NOISE_RECORD_PLAINTEXT_LEN; +use super::frame_jsonrpc_message; +use crate::ExecServerError; + +#[test] +fn fragments_and_reassembles_large_jsonrpc_message() { + let message = JSONRPCMessage::Notification(JSONRPCNotification { + method: "large/test".to_string(), + params: Some(serde_json::json!({ + "data": "x".repeat(128 * 1024), + })), + }); + let framed = frame_jsonrpc_message(&message).unwrap(); + assert!(framed.len() > 128 * 1024); + + let mut decoder = JsonRpcMessageDecoder::default(); + let mut decoded = Vec::new(); + for record in framed.chunks(NOISE_RECORD_PLAINTEXT_LEN) { + decoded.extend(decoder.push(record).unwrap()); + } + + assert_eq!(decoded, vec![message]); +} + +#[test] +fn rejects_declared_message_length_above_limit_without_payload() { + let mut decoder = JsonRpcMessageDecoder::default(); + let declared_len = (MAX_NOISE_JSONRPC_MESSAGE_LEN as u32 + 1).to_be_bytes(); + + assert!(matches!( + decoder.push(&declared_len), + Err(ExecServerError::Protocol(message)) + if message == "Noise relay JSON-RPC message has invalid length" + )); +} + +#[test] +fn rejects_oversized_plaintext_record() { + let mut decoder = JsonRpcMessageDecoder::default(); + + assert!(matches!( + decoder.push(&vec![0; NOISE_RECORD_PLAINTEXT_LEN + 1]), + Err(ExecServerError::Protocol(message)) + if message == "Noise relay plaintext record exceeds maximum length" + )); +} diff --git a/codex-rs/exec-server/src/noise_relay/mod.rs b/codex-rs/exec-server/src/noise_relay/mod.rs new file mode 100644 index 000000000..5e2bddcd0 --- /dev/null +++ b/codex-rs/exec-server/src/noise_relay/mod.rs @@ -0,0 +1,31 @@ +mod harness; +mod message_framing; +mod ordered_ciphertext; + +use tokio_tungstenite::tungstenite::protocol::WebSocketConfig; + +use crate::ExecServerError; + +pub(crate) use harness::NoiseHarnessConnectionArgs; +pub(crate) use harness::noise_harness_connection_from_websocket; + +// This bounds allocation in tungstenite before protobuf and Noise record +// validation run. It comfortably fits one maximum Noise record plus metadata. +const MAX_NOISE_RELAY_WEBSOCKET_MESSAGE_SIZE: usize = 256 * 1024; + +/// Return the websocket limits required by every Noise relay endpoint. +pub(crate) fn noise_relay_websocket_config() -> WebSocketConfig { + WebSocketConfig::default() + .max_frame_size(Some(MAX_NOISE_RELAY_WEBSOCKET_MESSAGE_SIZE)) + .max_message_size(Some(MAX_NOISE_RELAY_WEBSOCKET_MESSAGE_SIZE)) +} + +fn take_next_sequence(next_seq: &mut u32) -> Result { + // Never wrap: relay sequence is the explicit ordering key for an implicit + // Noise nonce. Reusing zero after u32::MAX would be ambiguous and unsafe. + let seq = *next_seq; + *next_seq = next_seq.checked_add(1).ok_or_else(|| { + ExecServerError::Protocol("Noise relay sequence number exhausted".to_string()) + })?; + Ok(seq) +} diff --git a/codex-rs/exec-server/src/noise_relay/ordered_ciphertext.rs b/codex-rs/exec-server/src/noise_relay/ordered_ciphertext.rs new file mode 100644 index 000000000..92bbd291d --- /dev/null +++ b/codex-rs/exec-server/src/noise_relay/ordered_ciphertext.rs @@ -0,0 +1,70 @@ +use std::collections::BTreeMap; + +use crate::ExecServerError; + +const MAX_REORDER_DISTANCE: u32 = 64; +const MAX_PENDING_BYTES: usize = 1024 * 1024; + +/// Reorders relay records before they reach Noise's implicit receive nonce. +/// The window is bounded, and each sequence number is released at most once. +#[derive(Default)] +pub(crate) struct OrderedCiphertextFrames { + next_seq: u32, + pending: BTreeMap>, + pending_bytes: usize, +} + +impl OrderedCiphertextFrames { + /// Accept one relay record and return the newly contiguous ciphertext run. + /// + /// Returns nothing for duplicates or while a gap remains. Closing a gap also + /// releases any buffered records that now follow it contiguously. + pub(crate) fn push( + &mut self, + seq: u32, + payload: Vec, + ) -> Result>, ExecServerError> { + // Keep the first ciphertext for a sequence. Later copies are duplicates. + if seq < self.next_seq || self.pending.contains_key(&seq) { + return Ok(Vec::new()); + } + if seq > self.next_seq { + // Bound both the sequence gap and buffered bytes. + if seq - self.next_seq > MAX_REORDER_DISTANCE { + return Err(ExecServerError::Protocol( + "Noise relay ciphertext exceeds reorder window".to_string(), + )); + } + let pending_bytes = self.pending_bytes + payload.len(); + if pending_bytes > MAX_PENDING_BYTES { + return Err(ExecServerError::Protocol( + "Noise relay pending ciphertext buffer is full".to_string(), + )); + } + self.pending.insert(seq, payload); + self.pending_bytes = pending_bytes; + return Ok(Vec::new()); + } + + // Release the expected record and anything now contiguous behind it. + let mut ready = vec![payload]; + self.advance()?; + while let Some(payload) = self.pending.remove(&self.next_seq) { + self.pending_bytes -= payload.len(); + ready.push(payload); + self.advance()?; + } + Ok(ready) + } + + fn advance(&mut self) -> Result<(), ExecServerError> { + self.next_seq = self.next_seq.checked_add(1).ok_or_else(|| { + ExecServerError::Protocol("Noise relay sequence number exhausted".to_string()) + })?; + Ok(()) + } +} + +#[cfg(test)] +#[path = "ordered_ciphertext_tests.rs"] +mod tests; diff --git a/codex-rs/exec-server/src/noise_relay/ordered_ciphertext_tests.rs b/codex-rs/exec-server/src/noise_relay/ordered_ciphertext_tests.rs new file mode 100644 index 000000000..6aa86fcdf --- /dev/null +++ b/codex-rs/exec-server/src/noise_relay/ordered_ciphertext_tests.rs @@ -0,0 +1,52 @@ +use pretty_assertions::assert_eq; + +use super::MAX_PENDING_BYTES; +use super::OrderedCiphertextFrames; + +#[test] +fn releases_ciphertexts_only_in_nonce_order() { + let mut frames = OrderedCiphertextFrames::default(); + + assert_eq!( + frames.push(/*seq*/ 1, b"second".to_vec()).unwrap(), + Vec::>::new() + ); + assert_eq!( + frames.push(/*seq*/ 0, b"first".to_vec()).unwrap(), + vec![b"first".to_vec(), b"second".to_vec()] + ); +} + +#[test] +fn ignores_duplicate_ciphertexts_without_replacing_buffered_record() { + let mut frames = OrderedCiphertextFrames::default(); + + assert_eq!( + frames.push(/*seq*/ 1, b"first copy".to_vec()).unwrap(), + Vec::>::new() + ); + assert_eq!( + frames.push(/*seq*/ 1, b"replacement".to_vec()).unwrap(), + Vec::>::new() + ); + assert_eq!( + frames.push(/*seq*/ 0, b"zero".to_vec()).unwrap(), + vec![b"zero".to_vec(), b"first copy".to_vec()] + ); + assert_eq!( + frames.push(/*seq*/ 0, b"duplicate".to_vec()).unwrap(), + Vec::>::new() + ); +} + +#[test] +fn rejects_unbounded_reordering() { + let mut frames = OrderedCiphertextFrames::default(); + + assert!(frames.push(/*seq*/ 65, Vec::new()).is_err()); + assert!( + frames + .push(/*seq*/ 1, vec![0; MAX_PENDING_BYTES + 1]) + .is_err() + ); +} diff --git a/codex-rs/exec-server/src/proto/codex.exec_server.relay.v1.proto b/codex-rs/exec-server/src/proto/codex.exec_server.relay.v1.proto index 46527d80c..3de1bfbe9 100644 --- a/codex-rs/exec-server/src/proto/codex.exec_server.relay.v1.proto +++ b/codex-rs/exec-server/src/proto/codex.exec_server.relay.v1.proto @@ -14,6 +14,7 @@ message RelayMessageFrame { RelayResume resume = 7; RelayReset reset = 8; RelayHeartbeat heartbeat = 9; + RelayHandshake handshake = 10; } } @@ -35,3 +36,7 @@ message RelayReset { } message RelayHeartbeat {} + +message RelayHandshake { + bytes payload = 1; +} diff --git a/codex-rs/exec-server/src/proto/codex.exec_server.relay.v1.rs b/codex-rs/exec-server/src/proto/codex.exec_server.relay.v1.rs index 072a003da..f65e1329f 100644 --- a/codex-rs/exec-server/src/proto/codex.exec_server.relay.v1.rs +++ b/codex-rs/exec-server/src/proto/codex.exec_server.relay.v1.rs @@ -9,7 +9,7 @@ pub struct RelayMessageFrame { pub ack: u32, #[prost(uint32, tag = "4")] pub ack_bits: u32, - #[prost(oneof = "relay_message_frame::Body", tags = "5, 6, 7, 8, 9")] + #[prost(oneof = "relay_message_frame::Body", tags = "5, 6, 7, 8, 9, 10")] pub body: ::core::option::Option, } pub mod relay_message_frame { @@ -25,6 +25,8 @@ pub mod relay_message_frame { Reset(super::RelayReset), #[prost(message, tag = "9")] Heartbeat(super::RelayHeartbeat), + #[prost(message, tag = "10")] + Handshake(super::RelayHandshake), } } #[derive(Clone, PartialEq, ::prost::Message)] @@ -52,3 +54,8 @@ pub struct RelayReset { } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct RelayHeartbeat {} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct RelayHandshake { + #[prost(bytes = "vec", tag = "1")] + pub payload: ::prost::alloc::vec::Vec, +} diff --git a/codex-rs/exec-server/src/relay.rs b/codex-rs/exec-server/src/relay.rs index b0f9ed7a6..e0c4d7ccb 100644 --- a/codex-rs/exec-server/src/relay.rs +++ b/codex-rs/exec-server/src/relay.rs @@ -23,24 +23,25 @@ use crate::connection::JsonRpcConnectionEvent; use crate::connection::JsonRpcTransport; use crate::connection::WEBSOCKET_KEEPALIVE_INTERVAL; use crate::relay_proto::RelayData; +use crate::relay_proto::RelayHandshake; use crate::relay_proto::RelayMessageFrame; use crate::relay_proto::RelayResume; use crate::relay_proto::relay_message_frame; use crate::server::ConnectionProcessor; const RELAY_MESSAGE_FRAME_VERSION: u32 = 1; - #[derive(Debug, Clone, Copy, Eq, PartialEq)] -enum RelayFrameBodyKind { +pub(crate) enum RelayFrameBodyKind { Data, Ack, Resume, Reset, Heartbeat, + Handshake, } impl RelayMessageFrame { - fn data(stream_id: String, seq: u32, payload: Vec) -> Self { + pub(crate) fn data(stream_id: String, seq: u32, payload: Vec) -> Self { Self { version: RELAY_MESSAGE_FRAME_VERSION, stream_id, @@ -55,7 +56,7 @@ impl RelayMessageFrame { } } - fn resume(stream_id: String) -> Self { + pub(crate) fn resume(stream_id: String) -> Self { Self { version: RELAY_MESSAGE_FRAME_VERSION, stream_id, @@ -67,7 +68,19 @@ impl RelayMessageFrame { } } - fn validate(&self) -> Result { + pub(crate) fn handshake(stream_id: String, payload: Vec) -> Self { + Self { + version: RELAY_MESSAGE_FRAME_VERSION, + stream_id, + ack: 0, + ack_bits: 0, + body: Some(relay_message_frame::Body::Handshake(RelayHandshake { + payload, + })), + } + } + + pub(crate) fn validate(&self) -> Result { if self.version != RELAY_MESSAGE_FRAME_VERSION { return Err(ExecServerError::Protocol(format!( "unsupported relay message frame version {}", @@ -99,27 +112,56 @@ impl RelayMessageFrame { Ok(RelayFrameBodyKind::Reset) } Some(relay_message_frame::Body::Heartbeat(_)) => Ok(RelayFrameBodyKind::Heartbeat), + Some(relay_message_frame::Body::Handshake(handshake)) => { + if handshake.payload.is_empty() { + return Err(ExecServerError::Protocol( + "relay handshake message frame is missing payload".to_string(), + )); + } + Ok(RelayFrameBodyKind::Handshake) + } None => Err(ExecServerError::Protocol( "relay message frame is missing body".to_string(), )), } } - fn into_jsonrpc_message(self) -> Result { + pub(crate) fn into_data(self) -> Result { let kind = self.validate()?; if kind != RelayFrameBodyKind::Data { return Err(ExecServerError::Protocol( "expected relay data message frame".to_string(), )); } - let payload = match self.body { - Some(relay_message_frame::Body::Data(data)) => data.payload, - _ => Vec::new(), - }; + match self.body { + Some(relay_message_frame::Body::Data(data)) => Ok(data), + _ => Err(ExecServerError::Protocol( + "expected relay data message frame".to_string(), + )), + } + } + + fn into_jsonrpc_message(self) -> Result { + let payload = self.into_data()?.payload; serde_json::from_slice(&payload).map_err(ExecServerError::Json) } - fn into_reset_reason(self) -> Option { + pub(crate) fn into_handshake_payload(self) -> Result, ExecServerError> { + let kind = self.validate()?; + if kind != RelayFrameBodyKind::Handshake { + return Err(ExecServerError::Protocol( + "expected relay handshake message frame".to_string(), + )); + } + match self.body { + Some(relay_message_frame::Body::Handshake(handshake)) => Ok(handshake.payload), + _ => Err(ExecServerError::Protocol( + "expected relay handshake message frame".to_string(), + )), + } + } + + pub(crate) fn into_reset_reason(self) -> Option { match self.body { Some(relay_message_frame::Body::Reset(reset)) if !reset.reason.is_empty() => { Some(reset.reason) @@ -129,16 +171,18 @@ impl RelayMessageFrame { } } -fn encode_relay_message_frame(frame: &RelayMessageFrame) -> Vec { +pub(crate) fn encode_relay_message_frame(frame: &RelayMessageFrame) -> Vec { frame.encode_to_vec() } -fn decode_relay_message_frame(payload: &[u8]) -> Result { +pub(crate) fn decode_relay_message_frame( + payload: &[u8], +) -> Result { RelayMessageFrame::decode(payload) .map_err(|err| ExecServerError::Protocol(format!("invalid relay message frame: {err}"))) } -fn jsonrpc_payload(message: &JSONRPCMessage) -> Result, ExecServerError> { +pub(crate) fn jsonrpc_payload(message: &JSONRPCMessage) -> Result, ExecServerError> { serde_json::to_vec(message).map_err(ExecServerError::Json) } @@ -304,7 +348,8 @@ where } RelayFrameBodyKind::Ack | RelayFrameBodyKind::Resume - | RelayFrameBodyKind::Heartbeat => {} + | RelayFrameBodyKind::Heartbeat + | RelayFrameBodyKind::Handshake => {} } } Some(Ok(Message::Close(_))) | None => { @@ -423,6 +468,7 @@ pub(crate) async fn run_multiplexed_environment( continue; } }; + let stream = streams.entry(stream_id.clone()).or_insert_with(|| { spawn_virtual_stream( stream_id.clone(), @@ -453,7 +499,8 @@ pub(crate) async fn run_multiplexed_environment( } RelayFrameBodyKind::Ack | RelayFrameBodyKind::Resume - | RelayFrameBodyKind::Heartbeat => {} + | RelayFrameBodyKind::Heartbeat + | RelayFrameBodyKind::Handshake => {} } } @@ -547,6 +594,7 @@ mod tests { use pretty_assertions::assert_eq; use tokio::net::TcpListener; use tokio::time::timeout; + use tokio_tungstenite::WebSocketStream; use tokio_tungstenite::accept_async; use tokio_tungstenite::connect_async; use tokio_tungstenite::tungstenite::Message; diff --git a/codex-rs/exec-server/src/relay_proto.rs b/codex-rs/exec-server/src/relay_proto.rs index b8a938b8c..4da5be5e6 100644 --- a/codex-rs/exec-server/src/relay_proto.rs +++ b/codex-rs/exec-server/src/relay_proto.rs @@ -2,6 +2,7 @@ mod generated; pub(crate) use generated::RelayData; +pub(crate) use generated::RelayHandshake; pub(crate) use generated::RelayMessageFrame; pub(crate) use generated::RelayResume; pub(crate) use generated::relay_message_frame;